2

重新認識 C# [6] - C# 6 大勢底定,錦上添花

 1 year ago
source link: https://blog.darkthread.net/blog/cs-in-depth-notes-7/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

C# 6 大勢底定,錦上添花-黑暗執行緒

【本系列是我的 C# in Depth 第四版讀書筆記,背景故事在這裡

(筆記跳過書本第六章 Async Implementation,該章深入剖析編譯器將 async/await 展開產生的狀態機程式實作細節,議題獨立且對日常開發幫助不太,未來有需要再看)

依我個人觀點,C# 2.0 加入泛型、C# 3.0 帶來 LINQ、C# 4 有 dynamic,C# 5 用 async/await 開啟了非同步時代,都包含革命性改變。至此,C# 語言功能已稱完整成熟,從 C# 6 開始,新功能及改良偏向錦上添花,不少則是 語法糖 (Syntax Sugar) 性質,加上部分主題過去寫過文章,年代較近記憶猶新,故 C# 6 之後的章節讀來相對輕鬆,我的筆記也會簡略一些,部分會用文章連結帶過,特此說明。

  • 自動實作屬性可加初始值
public class Person
{
    public List<Person> Friends { get; set; } = new List<Person>();
    public string FixedText { get; } = "Fixed"; // 唯讀
    public List<Person> Children { get; private set; } = new List<Person>(); // 唯讀,內部可修改
}
  • Definite Assignment Rules - Compiler 追蹤哪些變數有給值了
// C# 5
public struct Point
{
    public double X { get; private set; }
    public double Y { get; private set; }

    public Point(double x, double y) : this()
    {
        X = x;
        Y = y;
    }
}

// C# 6
public struct Point
{
    public double X { get; }
    public double Y { get; }

    public Point(double x, double y)
    {
        X = x; //允許在建構式寫入唯讀屬性,視同 Field
        Y = y;
    }
}
  • Expression-Bodied Member
// => Math.Sqrt(X * X + Y * Y) 稱為 Expression-Bodied Member,注意:它不是 Lambda Expression
// Jon 用 Fat Arrow 來稱呼「=>」符號
public double DistanceFromOrigin => Math.Sqrt(X * X + Y * Y);

public struct LocalDateTime
{
    public LocalDate Date { get; }
    public int Year => Date.Year; // 唯讀屬性
    public int Month => Date.Month;
    public int Day => Date.Day;

    public LocalTime TimeOfDay { get; }
    public int Hour => TimeOfDay.Hour;
    public int Minute => TimeOfDay.Minute;
    public int Second => TimeOfDay.Second;
}

public int NanosecondOfSecond =>
    (int) (NanosecondOfDay % NodaConstants.NanosecondsPerSecond);

// Method    
public static Point Add(Point left, Vector right) => left + right;
// Operator
public static Point operator+(Point left, Vector right) =>
    new Point(left.X + right.X, left.Y + right.Y);
    
public int this[int index]
{
    // 只用在 get, set 依需求用傳統寫法
    // 若 get 需要寫一堆邏輯,代表可能該寫成方法
    get => values[index]; 
    set
    {
        if (value < 0)
        {
            throw new ArgumentOutOfRangeException();
        }
        Values[index] = value;
    }
}    
    
  • C# 6 不能用 Expression-Bodied Member 的場合:建構式、Finailizer、可讀寫或唯寫屬性或索引子、事件。但 C# 7 已解除此限制
  • 不適合 Expression-Bodied Member 的場合:需要檢查參數、變數需要解釋
public ZonedDateTime InZone(
    DateTimeZone zone,
    ZoneLocalMappingResolver resolver)
{
    Preconditions.CheckNotNull(zone);
    Preconditions.CheckNotNull(resolver);
    return zone.ResolveLocal(this, resolver);
}
//硬要的話可以這樣搞,但可讀性不佳
public ZonedDateTime InZone(
    DateTimeZone zone,
    ZoneLocalMappingResolver resolver) =>
    Preconditions.CheckNotNull(zone)
        .ResolveLocal(
            this,
            Preconditions.CheckNotNull(resolver));


public int Minute
{
    get
    {
        int minuteOfDay = (int) NanosecondOfDay / NanosecondsPerMinute;
        return minuteOfDay % MinutesPerHour;
    }
}

//硬改後,
public int Minute => minuteOfDay 的解釋功能沒了
    ((int) NanosecondOfDay / NodaConstants.NanosecondsPerMinute) %
    NodaConstants.MinutesPerHour;

var dateOfBirth = new DateTime(1976, 6, 19);
FormattableString formattableString =
    $"Jon was born on {dateofBirth:d}";
var culture = CultureInfo.GetCultureInfo("en-US");
var result = formattableString.ToString(culture);

//幾種套用指定國別格式的做法
DateTime date = DateTime.UtcNow;

string parameter1 = string.Format(
    CultureInfo.InvariantCulture,
    "x={0:yyyy-MM-dd}",
    date);

string parameter2 =
    ((FormattableString)$"x={date:yyyy-MM-dd}")
    .ToString(CultureInfo.InvariantCulture);

string parameter3 = FormattableString.Invariant(
    $"x={date:yyyy-MM-dd}");

string parameter4 = Invariant($"x={date:yyyy-MM-dd}");
  • FormattableString 的另類應用 - 從 SQL 指令自動產生參數
var tag = Console.ReadLine();
using (var conn = new SqlConnection(connectionString))
{
    conn.Open();
    using (var command = conn.NewSqlCommand(
        $@"SELECT Description FROM Entries
           WHERE Tag={tag:NVarChar}
           AND UserId={userId:Int}"))
    {
        // 轉成 Tag=@p1 UserId=@p2,並自動產生 SqlParameter
        using (var reader = command.ExecuteReader())
        {
            // Use the data
        }
    }
}

public static class SqlFormattableString
{
    public static SqlCommand NewSqlCommand(                              
        this SqlConnection conn,FormattableString formattableString)    
    {
        SqlParameter[] sqlParameters = formattableString.GetArguments()  
            .Select((value, position) =>                                 
                new SqlParameter(Invariant($"@p{position}"), value))     
            .ToArray();                                                  
        object[] formatArguments = sqlParameters                         
            .Select(p => new FormatCapturingParameter(p))                
            .ToArray();                                                  
        string sql = string.Format(formattableString.Format,
            formatArguments);
        var command = new SqlCommand(sql, conn);                         
        command.Parameters.AddRange(sqlParameters);                      
        return command;
    }

    private class FormatCapturingParameter : IFormattable                
    {
        private readonly SqlParameter parameter;

        internal FormatCapturingParameter(SqlParameter parameter)       
        {
            this.parameter = parameter;
        }
        public string ToString(string format, IFormatProvider formatProvider)
        {
            if (!string.IsNullOrEmpty(format))                              
            {                                                               
                parameter.SqlDbType = (SqlDbType) Enum.Parse(
                    typeof(SqlDbType), format, true);
            }                                                               
            return parameter.ParameterName;                               
        }
    }
}
//一些特殊案例
// 泛型型別,用 typeof 吧
nameof(Action<string>) //"Action"
nameof(Action<string, string>) //"Action"

static string Method<T>() => nameof(T); //得到 "T"

using GuidAlias = System.Guid;
nameof(GuidAlias); //"GuidAlias"

nameof(float) //System.Single
nameof(Guid?) //
nameof(String[])
  • Importing Static Members
using static System.Reflection.BindingFlags;
var fields = type.GetFields(Instance | Static | Public | NonPublic);

using static System.Net.HttpStatusCode;
switch (response.StatusCode)
{
    case OK:
    //...
    case TemporaryRedirect:
    case Redirect:
    case RedirectMethod:
    //...
    case NotFound:
    //...
    default:
    //...
}

// 花式用法
using static System.String;
...
string[] elements = { "a", "b" };
Console.WriteLine(Join(" ", elements));  
  • 初始化支援
StringBuilder builder = new StringBuilder(text)             
{                                                           
    Length = 10,
    [9] = '\u2026' //Indexer
};

var collectionInitializer = new Dictionary<string, int>
{
    { "A", 20 },
    { "B", 30 },
    { "B", 40 }
};

var objectInitializer = new Dictionary<string, int>
{
    ["A"] = 20,
    ["B"] = 30,
    ["B"] = 40 //B重複,編譯OK,執行出錯
};

// 意不意外,這樣是 OK 的
List<string> strings = new List<string>
{
    10,
    "hello",
    { 20, 3 }
};
// 因為它等於
List<string> strings = new List<string>();
strings.Add(10);
strings.Add("hello");
strings.Add(20, 3);
// 註:Add(20,3) 靠擴充方法
public static class StringListExtensions
{
    public static void Add(
    this List<string> list, int value, int count = 1)
    {
        list.AddRange(Enumerable.Repeat(value.ToString(), count));
    }
}

// 另一個案例
Person jon = new Person
{
    Name = "Jon",
    Contacts = { allContacts.Where(c => c.Town == "Reading") }
};
static class ListExtensions
{
    public static void Add<T>(this List<T> list, IEnumerable<T> collection)
    {
        list.AddRange(collection);
    }
}

  • Null Conditional Operator 用 "?" 簡化對 null 的處理
var readingCustomers = allCustomers
    .Where(c => c.Profile != null &&
                c.Profile.DefaultShippingAddress != null &&
                c.Profile.DefaultShippingAddress.Town == "Reading");
//簡化
var readingCustomers = allCustomers
    .Where(c => c.Profile?.DefaultShippingAddress?.Town == "Reading");
    
//背後
string result;
var tmp1 = c.Profile;
if (tmp1 == null) 
    result = null;
else
{
    var tmp2 = tmp1.DefaultShippingAddress;
    if (tmp2 == null)
        result = null;
    else
        result = tmp2.Town;
}
return result == "Reading";
  • r = x.SomeProp?.Equals("...") 傳回的是 bool? ,結果可能是 true/false/null,若結果是 null,r == true 為 false,r != false 為 true,要小心。
    x.SomeProp?.Equals("...") ?? true - null 時成立
    x.SomeProp?.Equals("...") ?? false - null 時不成立
  • ? 用在 Array 及索引子
int[] array = null;
int? firstElement = array?[0];
  • ? 用在事件
EventHandler handler = Click;
if (handler != null)
{
    handler(this, EventArgs.Empty);
}
// 簡化為
Click?.Invoke(this, EventArgs.Empty);
  • ? 用於可有可無的 XML 節點
string authorName = book.Element("author")?.Attribute("name")?.Value;
string authorName = (string) book.Element("author")?.Attribute("name");
  • 不適用 ? 的場合
person?.Name = "";
stats?.RequestCount++;
array?[index] = 10;
  • Exception Filter:catch 時加入額外檢查條件
try
{
    ...
}
catch (WebException e)
    when (e.Status == WebExceptionStatus.ConnectFailure)
{    
    ...
}

string[] messages =
{
    "You can catch this",
    "You can catch this too",
    "This won't be caught"
};
foreach (string message in messages)
{
    try
    {
        throw new Exception(message);
    }
    catch (Exception e)
        when (e.Message.Contains("catch"))
    {
        Console.WriteLine($"Caught '{e.Message}'");
    }
}
  • 例外採 Two-Pass Model (可能源自 Windows Structured Exception Handling (SEH) )
    try catch finally 多層之的觸發順序如下
static bool LogAndReturn(string message, bool result)
{
    Console.WriteLine(message);
    return result;
}

static void Top()
{
    try
    {
        throw new Exception();
    }
    finally
    {
        Console.WriteLine("Top finally");
    }
}

static void Middle()
{
    try
    {
        Top();
    }
    catch (Exception e)
        when (LogAndReturn("Middle filter", false))
    {
        Console.WriteLine("Caught in middle");
    }
    finally
    {
        Console.WriteLine("Middle finally");
    }
}

static void Bottom()
{
    try
    {
        Middle();
    }
    catch (IOException e)
        when (LogAndReturn("Never called", true))
    {
    }
    catch (Exception e)
        when (LogAndReturn("Bottom filter", true))
    {
        Console.WriteLine("Caught in Bottom");
    }
}

static void Main()
{
    Bottom();
}
/*
順序是
Middle filter
Bottom filter
Top finally # 注意:catch 到,2nd Pass 開始,由層到外跑 finally
Middle finally
Caught in Bottom
*/

以上行為可能產生的安全問題:try 提高權限 finally 調回權限,惡意呼叫端可以用 Exception Filter 搶在 finally 降權限前跑程式

  • 同 Exception 捕捉多次
try
{
    ...
}
catch (WebException e)
    when (e.Status == WebExceptionStatus.ConnectFailure)
{
    ...
}
catch (WebException e)
    when (e.Status == WebExceptionStatus.NameResolutionFailure)
{
    ...
}
static T Retry<T>(Func<T> operation, int attempts)
{   
    while (true)
    {
        try
        {
            attempts--;
            return operation();
        }
        catch (Exception e) when (attempts > 0)
        {
            Console.WriteLine($"Failed: {e}");
            Console.WriteLine($"Attempts left: {attempts}");
            Thread.Sleep(5000);
        }
    }
}
  • 用 ExceptionFilter 記 Log
static void Main()
{
    try
    {
        threw new SomeException("Bang!");
    }
    catch (Exception e) when (Log(e))
    {
    }
}        
static bool Log(Exception e)
{
    Console.WriteLine($"{DateTime.UtcNow}: {e.GetType()} {e.Message}");
    return false;
}
  • 型別測試 ExceptionFilter
catch (Exception tmp) when (tmp is IOException)
{
    IOException e = (IOException) tmp;
    ...
}
  • ExceptionFilter 跟 catch 再檢查條件的不同點:
    1. when 執行時還沒進入 2nd Pass,裡層 finally 還沒執行
    2. throw 的話,catch 層也會記入 Stacktrace
catch (Exception e) when (condition)
{
    ...
}

catch (Exception e)
{
    if (!condition)
    {
        throw;
    }
    ...
}

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK