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

(2022 年初訂下的目標,趁著最後三天,還是把它完成吧~)

區域方法 - 在方法裡可以寫方法。

static void Main()
{
    int x = 10;
    PrintAndIncrementX();
    PrintAndIncrementX();
    Console.WriteLine($"After calls, x = {x}");

    // 不適用 public, extern, virtual, new, override, static, abstract 修飾字
    // 不能套 Attribute [attrName]
    // 不支援多載(不能多個區域方法名稱相同僅有參數不同)
    // 可以用 async, unsafe,適用 Iterator Block
    // 支援泛型、選擇性參數
    // 要捕捉的變數需在之前先宣告過並給值
    // 不能捕捉函式的 ref 參數
    // 可以捕捉 this (但在 Struct 中不行)
    void PrintAndIncrementX()
    {
        Console.WriteLine($"x = {x}");
        x++;
    }
}

Compiler 會比照匿名方法產生相關程式(各版 Compiler 的實作可能有差異),但區域方法比匿名方法有效率,理由是它的生命週期短,不會傳到外部使用,可省去一些額外機制。

static void Main()
{
    int i = 0;
    AddToI(5);
    AddToI(10);
    Console.WriteLine(i);
    void AddToI(int amount) => i += amount;
}

//Roslyn 編譯成
private struct MainLocals
{
    public int i;
}

static void Main()
{
    MainLocals locals = new MainLocals();
    locals.i = 0;
    AddToI(5, ref locals);
    AddToI(10, ref locals);
    Console.WriteLine(locals.i);
}

static void AddToI(int amount, ref MainLocals locals)
{
    locals.i += amount;
}

區域方法適合應用在同一段邏輯在方法內重複使用多次的情況,善用變數捕捉特性比用參數傳遞更有效率(Value Type 變數不需複製)。

一個善用區域方法的例子,Iterator Block 方法在 foreach 迴圈前提早檢查變數:

// 要寫兩個方法以實現提早檢查參數錯誤,否則會有以下狀況
// var res = x.Select(...); <= 沒發現 x 是 null
// ...
// foreach (var item in res) <= 此時才報錯

public static IEnumerable<TResult> Select<TSource, TResult>(
    this IEnumerable<TSource> source,
    Func<TSource, TResult> selector)
{
    Preconditions.CheckNotNull(source, nameof(source));
    Preconditions.CheckNotNull(
        selector, nameof(selector));
    return SelectImpl(source, selector);
}

private static IEnumerable<TResult> SelectImpl<TSource, TResult>(
    IEnumerable<TSource> source,
    Func<TSource, TResult> selector)
{
    foreach (TSource item in source)
    {
        yield return selector(item);
    }
}

// 用區域方法簡化
public static IEnumerable<TResult> Select<TSource, TResult>(
    this IEnumerable<TSource> source,
    Func<TSource, TResult> selector)
{
    Preconditions.CheckNotNull(source, nameof(source));
    Preconditions.CheckNotNull(selector, nameof(selector));
    return SelectImpl(source, selector);

    // 也可以不要用參數傳 source 及 selector
    // 但捕捉會增加底層變數宣告(這兩個是 Ref Type),作者選擇考慮此蒜皮效能提升
    IEnumerable<TResult> SelectImpl(
        IEnumerable<TSource> validatedSource,
        Func<TSource, TResult> validatedSelector)
    {
        foreach (TSource item in validatedSource)
        {
            yield return validatedSelector(item);
        }
    }
    
    // 改用區域方法,寫法大幅簡化
    IEnumerable<TResult> SelectImpl()
    {
        foreach (TSource item in source)
        {
            yield return selector(item);
        }
    }
}

作者重申 - 學到新東西,不要為用而用,別為了炫技犧牲可讀性。

out var 寫法可簡化 out 參數使用:

static int? ParseInt32(string text)
{
    int value;
    return int.TryParse(text, out value) ? value : (int?) null;
}

// C# 7 更精簡,免事先宣告
static int? ParseInt32(string text) =>
    int.TryParse(text, out int value) ? value : (int?) null;
    
// 變數只有成功執行後才會賦,複雜一點的例子
static int? ParseAndSum(string text1, string text2) =>
    int.TryParse(text1, out int value1) &&
    int.TryParse(text2, out int value2)
    ? value1 + value2 : (int?) null;

C# 7.3 開始,out 可用在 this(...) 及 base(...)。

C# 7 開始支援二進位數字寫法 byte b3 = 0b10000111; 及底線分隔字元(Underscore Separator) byte b3 = 0b1000_0111; int bigNo = 123_456_789;

C# 7 的 throw 可以寫在更多地方:

public void UnimplementedMethod() 
    throw new NotImplementedException();

public void TestPredicateNeverCalledOnEmptySequence()
{
    int count = new string[0]
        .Count(x => throw new Exception("Bang!"));
    Assert.AreEqual(0, count);
}

public static T CheckNotNull<T>(T value, string paramName) where T : class
    => value ?? // 這個很方便
    throw new ArgumentNullException(paramName);

public static Name =>
    initialized
    ? data["name"]
    : throw new Exception("..."); // 這個也好用

C# 7.1 提供更簡便的 default(T) 寫法:

public async Task<string> FetchValueAsync(
    string key,
    CancellationToken cancellationToken = default(CancellationToken))
// 簡化為 default,交給 Compiler 去推斷 T 是什麼
public async Task<string> FetchValueAsync(
    string key, CancellationToken cancellationToken = default)    

C# 7.2 支援非結尾參數也具名(以前必須是結尾的參數才能用):

client.UploadCsv(table, schema: null, csvData, options);
// 比較能知道 null 是什麼意思,提高可讀性

C# 7.2 新增 private protected 修飾詞,比 protected 或 internal 更侷限,限定同一組件中的繼承類別才能存取。protected internal 則是對同組件「或」繼承類別開放。

C# 7.3 加入新的泛型限制:

static void EnumMethod<T>() where T : struct, Enum {}enum SampleEnum {}
static void EnumMethod<T>() where T : struct, Enum {}
static void DelegateMethod<T>() where T : Delegate {}
static void UnmanagedMethod<T>() where T : unmanaged {}
...
EnumMethod<SampleEnum>(); //OK
EnumMethod<Enum>(); // 無效K,不是 struct

DelegateMethod<Action>();
DelegateMethod<Delegate>();
DelegateMethod<MulticastDelegate>(); // 以上全部有效

UnmanagedMethod<int>(); // OK
UnmanagedMethod<string>(); // 無效,字串不是 Unmanaged 型別

C# 7.3 讓 Overloading 判斷更聰明:偵測泛型限制、Instance 方法叫 Instance 方法、靜態方法叫靜態方法。

// 為了套 DemoAttribute 到 name 欄位,得回頭用舊屬性做法
[Demo]
private string name;
public string Name
{
    get { return name; }
    set { name = value; }
}

// C# 7.3 提供的新寫法
[field: Demo]
public string Name { get; set; }

C# in Depth 第四版的最後一章,講的是當時仍在設計階段,尚未定案的 C# 8,而微軟官方文件剛好從 C# 8 開始提供各版本 C# 新功能介紹,因此我的 C# in Depth 就在 C# 7.3 劃上尾聲。

新年新希望最後居然沒有爛尾(謎:都拖了一整年還真有臉說),自己也嚇了一跳,而一路讀來補足了許多之前錯過或忽略的 C# 新功能,收獲超多,不過明年新希望好像要謹慎一點比較好,哈 XD

My notes for C# in Depth part 10


Comments

# by Antony

foreach拉!

# by Jeffrey

to Antony,已修正,謝。

Post a comment