【本系列是我的 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;
    }
    ...
}

My notes for C# in Depth part 7


Comments

Be the first to post a comment

Post a comment