重新認識 C# [9] - C# 7 的其他小確幸 (筆記完結)
2 |
【本系列是我的 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,已修正,謝。