2

重新認識 C# [7] - C# 7 Tuple 解構、Pattern Match、in、Span<T>

 2 years ago
source link: https://blog.darkthread.net/blog/cs-in-depth-notes-9/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

C# 7 Tuple 解構、Pattern Match、in、Span-黑暗執行緒

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

swith 加入 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)];
        }
    });

除了 Sapn<T>,還有 ReadOnlySpan<T>、Memroy<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)
{

}

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK