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

C# 7 提供三種導入區域變數的新做法:Destruction (解構)、Pattern、Out 變數

解構是指將 Tuple 的元素拆解回獨立變數,C# 7 加入了簡潔寫法,深得我心,例如:

var tuple = (10, "text");

var (n1, t1) = tuple;

// 以下寫法跟宣告新 ValueTuple 變數 (int n2, stirng n2) x = tuple 很像,但意義完全不同
(int n2, string t2) = tuple;

int n3;
string t3;
(n3, t3) = tuple;

// 型別可以明確宣告,也可以用 var
(int n4, var t4) = tuple;

// n1-n3 皆為 10,t1-t3 皆為 "text"
Console.WriteLine($"n1: {n1}; t1: {t1}");
Console.WriteLine($"n2: {n2}; t2: {t2}");
Console.WriteLine($"n3: {n3}; t3: {t3}");
Console.WriteLine($"n4: {n4}; t4: {t4}");

// 接其他方法傳回結果
(avg, min, max) = Calculate();

// 型別宣告不能內外混用
var (a, long b, name) = blah(); //不合法

// 不想要的資料丟給 _
var tuple = (1, 2, 3, 4);
var (x, y, _, _) = tuple;
//Error CS0103: The name ’_’ doesn’t exist in the current context
Console.WriteLine(_);
// 你還是先宣告 _ 把它當一般變數,但建議不要,就保留它用來接廢棄資料

// 現有變數及新變數混用
int x = -1;
(x, int y) = (1, 2);

延伸閱讀:C# 區域函式傳回多元資料的做法選擇

令人腦洞大開的簡潔寫法:

public sealed class Point
{
    public double X { get; }
    public double Y { get; }

    public Point(double x, double y) => (X, Y) = (x, y);
}

可能會造成混淆的寫法:

StringBuilder builder = new StringBuilder("12345");
StringBuilder original = builder;

(builder, builder.Length) =
    (new StringBuilder("67890"), 3);

Console.WriteLine(original); //"123"
Console.WriteLine(builder); //"67890"

// builder 是區域變數可以直接指定,builder.Length 是屬性,要修改會產生一個暫存變數
// 想成
StringBuilder tmp = builder;
(StringBuilder, int) tuple = (new StringBuilder("67890"), 3);
builder = tuple.Item1;
tmp.Length = tuple.Item2;

Tuple Literal 解構

// 也可用在 Func (但我個人覺得可讀性沒有很好,不推)
(string text, Func<int, int> func) = (null, x => x * 2);
(text, func) = ("text", x => x * 3);

// 轉型有效性跟一般 byte x = 5 相同
(byte x, byte y) = (5, 10); // 可以

非 Tuple 要解構的話,需實作 Deconstruct() 方法:

// Point 實作Deconstruct方法
public void Deconstruct(out double x, out double y)
{
    x = X;
    y = Y;
}
// 比照建講式的簡潔的寫法
public Point(double x, double y) => (X, Y) = (x, y);
public void Deconstruct(out double x, out double y) => (x, y) = (X, Y);

var point = new Point(1.5, 20);
// 解構時呼叫 Deconstruct()
var (x, y) = point;
Console.WriteLine($"x = {x}");
Console.WriteLine($"y = {y}");

// 也可以用擴充方法變魔術 (考慮多載)
static void Deconstruct(
    this DateTime dateTime,
    out int year, out int month, out int day) =>
    (year, month, day) =
    (dateTime.Year, dateTime.Month, dateTime.Day);

static void Deconstruct(
    this DateTime dateTime,
    out int year, out int month, out int day,
    out int hour, out int minute, out int second) =>
    (year, month, day, hour, minute, second) =
    (dateTime.Year, dateTime.Month, dateTime.Day,
    dateTime.Hour, dateTime.Minute, dateTime.Second);

static void Main()
{
    DateTime birthday = new DateTime(1976, 6, 19);
    DateTime now = DateTime.UtcNow;

    var (year, month, day, hour, minute, second) = now;
    (year, month, day) = birthday;
}

Pattern Match 在 Functional 語言用很久了,C# 7 開始導入。

// 情境多時 switch
static double Perimeter(Shape shape)
{
    switch (shape)
    {
        case null:
            throw new ArgumentNullException(nameof(shape));
        case Rectangle rect: // 型別相符的話,放入 rect 變數
            return 2 * (rect.Height + rect.Width);
        case Circle circle:
            return 2 * PI * circle.Radius;
        case Triangle tri:
            return tri.SideA + tri.SideB + tri.SideC;
        default:
            throw new ArgumentException(...);
    }
}


// 情境少用 is
static void CheckType<T>(object value)
{
    if (value is T t)
        Console.WriteLine($"Yes! {t} is a {typeof(T)}");
    else
        Console.WriteLine($"No! {value ?? "null"} is not a {typeof(T)}");
}

static void Main()
{
    CheckType<int?>(null);
    CheckType<int?>(5);     
    CheckType<int?>("text");
    CheckType<string>(null);
    CheckType<string>(5);
    CheckType<string>("text");
}


// 用 is 判斷型別跟內容
static void Match(object input)
{
    if (input is "hello")
        Console.WriteLine("Input is string hello");
    else if (input is 5L)
        Console.WriteLine("Input is long 5");
    else if (input is 10)
        Console.WriteLine("Input is int 10");
    else
        Console.WriteLine("Input didn't match hello, long 5 or int 10");
}
static void Main()
{
    Match("hello");
    Match(5L);
    Match(7);
    Match(10);
    Match(10L); //注意 10L(long) 不是 10(int),object.Equals(x, 10) == false
}

Pattern Match 中使用 var

static double Perimeter(Shape shape)
{
    switch (shape ?? CreateRandomShape())
    {
        case Rectangle rect:
            return 2 * (rect.Height + rect.Width);
        case Circle circle:
            return 2 * PI * circle.Radius;
        case Triangle triangle:
            return triangle.SideA + triangle.SideB + triangle.SideC;
        case var actualShape:
            throw new InvalidOperationException(
                $"Shape type {actualShape.GetType()} perimeter unknown");
    }
}

is 時同時宣告變數讓程式更簡潔:

int length = GetObject() is string text ? text.Length : -1;

is 失敗,區域變數仍會宣告,這有時是優點:

if (input is string text)
{
    Console.WriteLine("Input was already a string; using that");
}
else if (input is StringBuilder builder)
{
    // 上面 is 沒成立,仍會宣告 text 變數可供利用
    Console.WriteLine("Input was a StringBuilder; using that");    
    text = builder.ToString();
}
else
{
    Console.WriteLine(
        $"Unable to use value of type ${input.GetType()}. Enter text:");    
    //填入 Fallback 內容
    text = Console.ReadLine();
}
Console.WriteLine($"Final result: {text}");

switch 加入 when 條件(Guard Clause):

static int Fib(int n)
{
    switch (n)
    {
        case 0: return 0;
        case 1: return 1;
        case var _ when n > 1: return Fib(n - 2) + Fib(n - 1);
        default: throw new ArgumentOutOfRangeException(
            nameof(n), "Input must be non-negative");
    }
}

private string GetUid(TypeReference type, bool useTypeArgumentNames)
{
    switch (type)
    {
        // 跟傳統 switch 不同,case 的順序會影響結果
        case ByReferenceType brt:
            return $"{GetUid(brt.ElementType, useTypeArgumentNames)}@";
        // gp 只在 case Body 內有效
        case GenericParameter gp when useTypeArgumentNames:
            return gp.Name;
        case GenericParameter gp when gp.DeclaringType != null:
            return $"`{gp.Position}";
        case GenericParameter gp when gp.DeclaringMethod != null:
            return $"``{gp.Position}";
        case GenericParameter gp:
            throw new InvalidOperationException(
                "Unhandled generic parameter");
        case GenericInstanceType git:
            return "(This part of the real code is long and irrelevant)";
        default:
            return type.FullName.Replace('/', '.');
    }
}

//多條件
static void CheckBounds(object input)
{
    switch (input)
    {
        case int x when x > 1000:
        case long y when y > 10000L:
            // 無法使用 x 或 y,不知道哪一個有值
            // CS0165 Use of unassigned local variable 'x'
            Console.WriteLine("Value is too large");
            break;
        case int x when x < -1000:
        case long y when y < -10000L:
            Console.WriteLine("Value is too low");
            break;
        default:
            Console.WriteLine("Value is in range");
            break;
    }
}

Ref 區域變數

int x = 10;
ref int y = ref x;
x++;
y++;
Console.WriteLine(x);
int z = 20;
y = ref z; // C# 7.3 之前不允許重設定

var array = new (int x, int y)[10];

for (int i = 0; i < array.Length; i++)
{
    array[i] = (i, i);
}

for (int i = 0; i < array.Length; i++)
{
    // ValueTuple 是 Struct
    // 若 var elem = array[i] 會 Copy 一份,改完還要 array[i] = elem
    // 再不然就是 array[i].x++, array[i].y *= 2
    ref var element = ref array[i];
    element.x++;
    element.y *= 2;
}

// 唯讀欄位限制
class MixedVariables
{
    private int writableField;
    private readonly int readonlyField;

    public void TryIncrementBoth()
    {
        ref int x = ref writableField;
        // CS0192 A readonly field cannot be used as a ref or out value (except in a constructor)
        ref int y = ref readonlyField; //不允許

        x++;
        y++; 
    }
}

// 只接受 Identity Conversion
(int x, int y) tuple1 = (10, 20);
ref (int a, int b) tuple2 = ref tuple1; // Identity Conversion,允許
tuple2.a = 30;
Console.WriteLine(tuple1.x);

Ref Return

static void Main()
{
    int x = 10;
    ref int y = ref RefReturn(ref x);
    y++;
    Console.WriteLine(x);
}

static ref int RefReturn(ref int p)
{
    // Compiler 會檢查 ref return 的對象不是在函式生出來的,而且會繼續存在
    return ref p;
}

// Indexer
class ArrayHolder
{
    private readonly int[] array = new int[10];
    public ref int this[int index] => ref array[index];
}

static void Main()
{
    ArrayHolder holder = new ArrayHolder();
    ref int x = ref holder[0];
    ref int y = ref holder[0];

    x = 20;
    Console.WriteLine(y);
}


// 結合 Conditional ?: Operator 
static (int even, int odd) CountEvenAndOdd(IEnumerable<int> values)
{
    var result = (even: 0, odd: 0);
    foreach (var value in values)
    {
        ref int counter = ref (value & 1) == 0 ?
            ref result.even : ref result.odd;
        counter++;
    }
    return result;
}

// C# 7.2 新增 ref readonly
static readonly int field = DateTime.UtcNow.Second;

static ref readonly int GetFieldAlias() => ref field;

static void Main()
{
    ref readonly int local = ref GetFieldAlias();
    Console.WriteLine(local);
}

C# 7.2 加入 in 參數,增加效能,但要留意傳 ref 會有過程中被其他來源修改的副作用,宜斟酌

// in 相當於 ref readonly,但呼叫端不需加註 in 也會套用
// IL 層會加上 [IsReadOnlyAttribute]
static void PrintDateTime(in DateTime value)
{
    string text = value.ToString(
        "yyyy-MM-dd'T'HH:mm:ss",
        CultureInfo.InvariantCulture);
    Console.WriteLine(text);
}

static void Main()
{
    DateTime start = DateTime.UtcNow;
    PrintDateTime(start);
    PrintDateTime(in start);
    PrintDateTime(start.AddMinutes(1)); //複製到隱藏區域變數送過去
    PrintDateTime(in start.AddMinutes(1)); //不允許
}

C# 7.2 Readonly Struct

傳統做法

public struct YearMonthDay
{
    public int Year { get; }
    public int Month { get; }
    public int Day { get; }

    public YearMonthDay(int year, int month, int day) =>
        (Year, Month, Day) = (year, month, day);
}

class ImplicitFieldCopy
{
    private readonly YearMonthDay readOnlyField =
        new YearMonthDay(2018, 3, 1);
    private YearMonthDay readWriteField =
        new YearMonthDay(2018, 3, 1);

    public void CheckYear()
    {
        // IL 層有額外的複製動作
        int readOnlyFieldYear = readOnlyField.Year;
        int readWriteFieldYear = readWriteField.Year;
    }
}

// C# 7.2 可在 struct 前加 readonly,IL 層動作會簡化
// 並強制 Compiler 檢查 Struct 行為都是唯讀的
public readonly struct YearMonthDay
{
    public int Year { get; }
    public int Month { get; }
    public int Day { get; }

    public YearMonthDay(int year, int month, int day) =>
        (Year, Month, Day) = (year, month, day);
}

在擴充方法使用 in 及 ref

public static double Magnitude(this in Vector3D vec) =>
    Math.Sqrt(vec.X * vec.X + vec.Y * vec.Y + vec.Z * vec.Z);
// ref 及 in 省去複製成本
public static void OffsetBy(this ref Vector3D orig, in Vector3D off) =>
    orig = new Vector3D(orig.X + off.X, orig.Y + off.Y, orig.Z + off.Z);

var vector = new Vector3D(1.5, 2.0, 3.0);
var offset = new Vector3D(5.0, 2.5, -1.0);
vector.OffsetBy(offset);
Console.WriteLine($"({vector.X}, {vector.Y}, {vector.Z})");
Console.WriteLine(vector.Magnitude());

// 宣告成 ref readonly 後不能呼叫 OffsetBy,因為 origin 是 ref,唯讀不符要求
ref readonly var alias = ref vector;
alias.OffsetBy(offset); //Error: trying to use a read-only variable as ref

// in ref 只適用 Value Type,以下做法不行
static void Method(this ref string target) // string 不是 Value Type
static void Method<T>(this ref T target) where T : IComparable<T> // 要 where T : struct,IComparable<T>
static void Method<T>(this in string target)
static void Method<T>(this in T target) where T : struct // in 不能放在型別參數上
static void Method<T>(this in Guid target, T other) // 這個可以

C# 7.2 Ref-Like Struct 永遠存放在 Stack

public ref struct RefLikeStruct
{
    //...
}

限制很多:(反正 Compiler 說不行就是不行)

  1. 不能當成非 Ref-Like Struct 的欄位,否則會導致儲存到 Heap 的行為。即使當成 Ref-Like Struct 的欄位,也不能是靜態的。
  2. 不可以 Boxing,如:object x = refLikeStruct;
  3. 不可以當成型別參數 (T) x、List<T>
  4. 不可以 typeof(RefLikeStruct[])
  5. 不能被捕捉(Closure)

(Jon 坦承他也不是很懂...)

Span<T> 是 Ref-Like Struct,以更有效率的方法存取記憶體資料。可以 Split,不需要複製資料就切割一段出來,新版 JIT 有針對 Span<T> 做了最佳化,能大幅提升效能。

// 亂數字串產生器
static string Generate(string alphabet, Random random, int length)
{
    char[] chars = new char[length];
    for (int i = 0; i < length; i++)
    {
        chars[i] = alphabet[random.Next(alphabet.Length)];
    }
    return new string(chars);
}
string alphabet = "abcdefghijklmnopqrstuvwxyz";
Random random = new Random();
Console.WriteLine(Generate(alphabet, random, 10));

// Unsafe 版
unsafe static string Generate(string alphabet, Random random, int length)
{
    char* chars = stackalloc char[length];
    for (int i = 0; i < length; i++)
    {
        chars[i] = alphabet[random.Next(alphabet.Length)];
    }
    return new string(chars);
}

// Span<T> 版本,不用 unsafe 做到一樣的事,但仍有複製行為發生
static string Generate(string alphabet, Random random, int length)
{
    Span<char> chars = stackalloc char[length];
    for (int i = 0; i < length; i++)
    {
        chars[i] = alphabet[random.Next(alphabet.Length)];
    }
    return new string(chars);
}

// String.<TState>(int length, TState state, SpanAction<char, TState> action)
// delegate void SpanAction<T, in TArg>(Span<T> span, TArg arg);
// 配置一段記憶體,建立 Span 指向字串內容記憶體,呼叫 action 填值
static string Generate(string alphabet, Random random, int length) =>
    // 準備一個 (alphabet, random) Tuple 當成 TState 傳給 action
    // 這麼做是為了避免捕捉變數 Compiler 建立 Conetxt 物件,一堆 Heap 動作
    // 當 Lambda 不用捕捉時,用 static 方法就可以了
    string.Create(length, (alphabet, random), (span, state) =>
    {
        var alphabet2 = state.alphabet;
        var random2 = state.random;
        for (int i = 0; i < span.Length; i++)
        {
            span[i] = alphabet2[random2.Next(alphabet2.Length)];
        }
    });

除了 Span<T>,還有 ReadOnlySpan<T>、Memory<T>、ReadOnlyMemory<T>,但水太深了,超出本書範圍。

C# 7.3 加入 stackalloc、Pattern-Based fixed

Span<int> span = stackalloc int[] { 1, 2, 3 };
int* pointer = stackalloc int[] { 4, 5, 6 };

fixed (int* ptr = value)
{

}

My notes for C# in Depth part 9


Comments

# by klcintw

最後一段「除了 Sapn<T>」應為Span<T>。

# by Jeffrey

to klcintw,謝,已修正。

# by tim

"swith 加入 when 條件(Guard Clause): " ,swith 應為 switch ?

# by Jeffrey

to tim, 感謝,已校正。

# by Juro

Memroy<T> 應為 Memory<T>

# by Jeffrey

to Juro, 感謝,已更正。

Post a comment