重新認識 C# [6] - C# 6 大勢底定,錦上添花
0 |
【本系列是我的 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;
- C# Interpolated Strings 字串插值
- C# 字串補空白與靠左右對齊
- 術語:Verbatim String Literals @"....",與字串插值併用時 $@"Text"
- C# 9/.NET 5 (含)以前,字串插值的底層用 String.Format 實作,C# 10 砍掉重練:Improved Interpolated Strings in C# 10
- .NET 4.6 加入 FormattableString,方便處理國別格式
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;
}
}
}
- nameof() C# 技巧:用列舉及 nameof 取代字串常數提高可維護性
//一些特殊案例
// 泛型型別,用 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)
{
...
}
- 簡單 Retry 機制,同場加映:
處理 Deadlock、網路瞬斷、伺服器忙線等暫時性故障的利器 - Polly
軟體系統的保險絲 - .NET Polly CircuitBreakerPolicy
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 再檢查條件的不同點:
- when 執行時還沒進入 2nd Pass,裡層 finally 還沒執行
- 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