C# 関数型プログラミングの詳細 (12) 不変性、匿名型、およびタプル

[C# シリーズ経由の LINQ]

[C# 関数型プログラミングの詳細シリーズ]

最新バージョン:https://weblogs.asp.net/dixin/functional-csharp-immutability-anonymous-type-and-tuple

不変性は機能的パラダイムの重要な側面です。前述のように、命令型/オブジェクト指向プログラミングは通常ステートフルであり、関数型プログラミングは状態変更なしの不変性を促進します。 C# プログラミングでは、さまざまな種類の不変性がありますが、2 つのレベルに分類できます。ある値の不変性と、ある値の内部状態の不変性です。ローカル変数を例にとると、ローカル変数は、一度割り当てられると再割り当てする方法がない場合、不変と呼ぶことができます。内部状態が初期化されると、その状態を別の状態に変更する方法がない場合、ローカル変数を不変と呼ぶこともできます。

一般に、不変性によってバグの主な原因が取り除かれるため、多くの場合、プログラミングが容易になります。不変値と不変状態は、本質的にスレッドセーフであるため、並行/並列/マルチスレッド プログラミングを大幅に簡素化することもできます。不変性の欠点は、明らかに、不変の値または不変の状態を変更するには、別の新しいインスタンスを突然変異で作成する必要があり、パフォーマンスのオーバーヘッドが発生する可能性があることです.

不変の値

多くの関数型言語は、不変の値をサポートしています。変数とは対照的です。値が何かに割り当てられると、再割り当てできないため、他の値に変更することはできません。たとえば、F# では、mutable キーワードが指定されていない限り、値は既定で不変です。

let value = new Uri("https://weblogs.asp.net/dixin") // Immutable value.
value <- null // Cannot be compiled. Cannot reassign to value.

let mutable variable = new Uri("https://weblogs.asp.net/dixin") // Mutable variable.
variable <- null // Can reassign to variable.

C に似た言語として、C# 変数はデフォルトで変更可能です。 C# には、不変値のための言語機能が他にもいくつかあります。

定数

C# には、実行時に変更できないコンパイル時定数を定義する const キーワードがあります。ただし、プリミティブ型、文字列、および null 参照に対してのみ機能します:

internal static partial class Immutability
{
    internal static void Const()
    {
        const int immutable1 = 1;
        const string immutable2 = "https://weblogs.asp.net/dixin";
        const object immutale3 = null;
        const Uri immutable4 = null;
        const Uri immutable5 = new Uri(immutable2); // Cannot be compiled.
    }
}

using ステートメントと foreach ステートメント

C# は、前述の using ステートメントや foreach ステートメントなど、いくつかのステートメントで不変値もサポートしています。

internal static void ForEach(IEnumerable<int> source)
{
    foreach (int immutable in source)
    {
        // Cannot reassign to immutable.
    }
}

internal static void Using(Func<IDisposable> disposableFactory)
{
    using (IDisposable immutable = disposableFactory())
    {
        // Cannot reassign to immutable.
    }
}

クラスのこのリファレンス

クラス定義では、このキーワードはインスタンス関数メンバーで使用できます。クラスの現在のインスタンスを参照し、不変です:

internal partial class Device
{
    internal void InstanceMethod()
    {
        // Cannot reassign to this.
    }
}

デフォルトでは、この参照は後で説明する構造定義のために変更可能です。

関数の読み取り専用入力と読み取り専用出力

読み取り専用参照 (パラメーター内) によって渡される前述の関数パラメーターは関数内で不変であり、読み取り専用参照 (ref readonly return) によって返される関数の結果は、関数の呼び出し元に対して不変です:

internal static void ParameterAndReturn<T>(Span<T> span)
{
    ref readonly T Last(in Span<T> immutableInput)
    {
        // Cannot reassign to immutableInput.
        int length = immutableInput.Length;
        if (length > 0)
        {
            return ref immutableInput[length - 1];
        }
        throw new ArgumentException("Span is empty.", nameof(immutableInput));
    }

    ref readonly T immutableOutput = ref Last(in span);
    // Cannot reassign to immutableOutput.
}

読み取り専用参照によるローカル変数 (ref readonly variable)

C# 7.2 では、ローカル変数の読み取り専用参照が導入されています。 C# では、既存のローカル変数を使用して新しいローカル変数を定義および初期化する場合、次の 3 つのケースがあります:

  • コピー:ローカル変数に直接割り当てます。値型インスタンスが割り当てられている場合、その値型インスタンスは新しいインスタンスにコピーされます。参照型インスタンスが割り当てられている場合、その参照がコピーされます。そのため、新しいローカル変数が再割り当てされても、以前のローカル変数は影響を受けません。
  • 参照:ref キーワードを使用してローカル変数に割り当てます。新しいローカル変数は、既存のローカル変数のポインターまたはエイリアスとして仮想的に表示できます。したがって、新しいローカル変数が再割り当てされると、以前のローカル変数が再割り当てされるのと同じです
  • 読み取り専用参照:ref readonly キーワードを使用してローカル変数に割り当てます。新しいローカル変数は、ポインタまたはエイリアスとして仮想的に表示することもできますが、この場合、新しいローカル変数は不変であり、再割り当てできません。
internal static void ReadOnlyReference()
{
    int value = 1;
    int copyOfValue = value; // Assign by copy.
    copyOfValue = 10; // After the assignment, value does not change.
    ref int mutaleRefOfValue = ref value; // Assign by reference.
    mutaleRefOfValue = 10; // After the reassignment, value changes too.
    ref readonly int immutableRefOfValue = ref value; // Assign by readonly reference.
    immutableRefOfValue = 0; // Cannot be compiled. Cannot reassign to immutableRefOfValue.

    Uri reference = new Uri("https://weblogs.asp.net/dixin");
    Uri copyOfReference = reference; // Assign by copy.
    copyOfReference = new Uri("https://flickr.com/dixin"); // After the assignment, reference does not change.
    ref Uri mutableRefOfReference = ref reference; // Assign by reference.
    mutableRefOfReference = new Uri("https://flickr.com/dixin"); // After the reassignment, reference changes too.
    ref readonly Uri immutableRefOfReference = ref reference; // Assign by readonly reference.
    immutableRefOfReference = null; // Cannot be compiled. Cannot reassign to immutableRefOfReference.
}

LINQ クエリ式の不変値

C# 3.0 で導入された LINQ クエリ式では、from、join、let 句で値を宣言でき、into クエリ キーワードでも値を宣言できます。これらの値はすべて不変です:

internal static void QueryExpression(IEnumerable<int> source1, IEnumerable<int> source2)
{
    IEnumerable<IGrouping<int, int>> query =
        from immutable1 in source1
        // Cannot reassign to immutable1.
        join immutable2 in source2 on immutable1 equals immutable2 into immutable3
        // Cannot reassign to immutable2, immutable3.
        let immutable4 = immutable1
        // Cannot reassign to immutable4.
        group immutable4 by immutable4 into immutable5
        // Cannot reassign to immutable5.
        select immutable5 into immutable6
        // Cannot reassign to immutable6.
        select immutable6;
}

クエリ式は、クエリ メソッド呼び出しの構文糖衣です。これについては、LINQ to Objects の章で詳しく説明します。

不変状態 (不変型)

インスタンスが不変型から構築されると、インスタンスの内部データは変更できなくなります。 C# では、文字列 (System.String) は不変型です。文字列が構築されると、その文字列を変更する API はありません。たとえば、string.Remove は文字列を変更しませんが、指定された文字が削除された、新しく構築された文字列を常に返します。対照的に、文字列ビルダー (System.Text.StringBuilder) は変更可能な型です。たとえば、StringBuilder.Remove は実際に文字列を変更して、指定された文字を削除します。コア ライブラリでは、ほとんどのクラスが可変型であり、ほとんどの構造体が不変型です。

型の定数フィールド

型 (クラスまたは構造体) を定義する場合、const 修飾子を持つフィールドは不変です。繰り返しますが、プリミティブ型、文字列、および null 参照に対してのみ機能します。

namespace System
{
    public struct DateTime : IComparable, IComparable<DateTime>, IConvertible, IEquatable<DateTime>, IFormattable, ISerializable
    {
        private const int DaysPerYear = 365;
        // Compiled to:
        // .field private static literal int32 DaysPerYear = 365

        private const int DaysPer4Years = DaysPerYear * 4 + 1;
        // Compiled to:
        // .field private static literal int32 DaysPer4Years = 1461

        // Other members.
    }
}

読み取り専用インスタンス フィールドを持つ不変クラス

フィールドに readonly 修飾子が使用されている場合、フィールドはコンストラクターによってのみ初期化でき、後で再割り当てすることはできません。したがって、すべてのインスタンス フィールドを読み取り専用として定義することで、不変クラスを不変にすることができます。

internal partial class ImmutableDevice
{
    private readonly string name;

    private readonly decimal price;
}

前述の auto プロパティのシンタックス シュガーを使用すると、読み取り専用フィールド定義を自動的に生成できます。以下は、読み取り/書き込み状態の可変データ型と、読み取り専用インスタンス フィールドに格納された読み取り専用状態の不変データ型の例です:

internal partial class MutableDevice
{
    internal string Name { get; set; }

    internal decimal Price { get; set; }
}

internal partial class ImmutableDevice
{
    internal ImmutableDevice(string name, decimal price)
    {
        this.Name = name;
        this.Price = price;
    }

    internal string Name { get; }

    internal decimal Price { get; }
}

どうやら、構築された MutableDevice インスタンスはフィールドによって格納された内部状態を変更できますが、ImmutableDevice インスタンスは変更できません:

internal static void State()
{
    MutableDevice mutableDevice = new MutableDevice() { Name = "Microsoft Band 2", Price = 249.99M };
    // Price drops.
    mutableDevice.Price -= 50M;

    ImmutableDevice immutableDevice = new ImmutableDevice(name: "Surface Book", price: 1349.00M);
    // Price drops.
    immutableDevice = new ImmutableDevice(name: immutableDevice.Name, price: immutableDevice.Price - 50M);
}

不変型のインスタンスは状態を変更できないため、バグの主な原因を取り除き、常にスレッドセーフです。しかし、これらのメリットには代償が伴います。一部の既存のデータを別の値に更新することは一般的です。たとえば、現在の価格に基づいて割引を適用します。

internal partial class MutableDevice
{
    internal void Discount() => this.Price = this.Price * 0.9M;
}

internal partial class ImmutableDevice
{
    internal ImmutableDevice Discount() => new ImmutableDevice(name: this.Name, price: this.Price * 0.9M);
}

価格を割引する場合、MutableDevice.Discount は状態を直接変更します。 ImmutableDevice.Discount はこれを行うことができないため、新しい状態で新しいインスタンスを構築し、これも不変である新しいインスタンスを返す必要があります。これはパフォーマンスのオーバーヘッドです。

多くの .NET 組み込み型は、ほとんどの値型 (プリミティブ型、System.Nullable、System.DateTime、System.TimeSpan など) と一部の参照型 (文字列、System.Lazy、System.Linq.Expressions.Expression およびその派生型など)。 Microsoft は、不変のコレクション System.Collections.Immutable の NuGet パッケージも提供しており、不変の配列、リスト、ディクショナリなどがあります。

不変構造 (読み取り専用構造)

次の構造体は、上記の不変クラスと同じパターンで定義されています。構造は不変に見えます:

internal partial struct Complex
{
    internal Complex(double real, double imaginary)
    {
        this.Real = real;
        this.Imaginary = imaginary;
    }

    internal double Real { get; }

    internal double Imaginary { get; }
}

auto プロパティのシンタックス シュガーを使用すると、読み取り専用フィールドが生成されます。ただし、構造体の場合、読み取り専用フィールドは不変性に十分ではありません。クラスとは対照的に、構造体のインスタンス関数メンバーでは、この参照は可変です:

internal partial struct Complex
{
    internal Complex(Complex value) => this = value; // Can reassign to this.

    internal Complex Value
    {
        get => this;
        set => this = value; // Can reassign to this.
    }

    internal Complex ReplaceBy(Complex value) => this = value; // Can reassign to this.

    internal Complex Mutate(double real, double imaginary) => 
        this = new Complex(real, imaginary); // Can reassign to this.
}

mutable this を使用すると、上記の構造は引き続き変更可能になります:

internal static void Structure()
{
    Complex complex1 = new Complex(1, 1);
    Complex complex2 = new Complex(2, 2);
    complex1.Real.WriteLine(); // 1
    complex1.ReplaceBy(complex2);
    complex1.Real.WriteLine(); // 2
}

このシナリオに対処するために、C# 7.2 では構造体定義の readonly 修飾子が有効になっています。構造体が不変であることを確認するために、すべてのインスタンス フィールドを読み取り専用に強制し、コンストラクターを除くインスタンス関数メンバーでこの参照を不変にします:

internal readonly partial struct ImmutableComplex
{
    internal ImmutableComplex(double real, double imaginary)
    {
        this.Real = real;
        this.Imaginary = imaginary;
    }

    internal ImmutableComplex(in ImmutableComplex value) => 
        this = value; // Can reassign to this only in constructor.

    internal double Real { get; }

    internal double Imaginary { get; }

    internal void InstanceMethod()
    {
        // Cannot reassign to this.
    }
}

不変の匿名型

C# 3.0 では、設計時に型定義を提供せずに、不変データを表す匿名型が導入されています。

internal static void AnonymousType()
{
    var immutableDevice = new { Name = "Surface Book", Price = 1349.00M };
}

型名は設計時には不明であるため、上記のインスタンスは匿名型であり、型名は var キーワードで表されます。コンパイル時に、次の不変データ型定義が生成されます:

[CompilerGenerated]
[DebuggerDisplay(@"\{ Name = {Name}, Price = {Price} }", Type = "<Anonymous Type>")]
internal sealed class AnonymousType0<TName, TPrice>
{
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private readonly TName name;

    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private readonly TPrice price;

    [DebuggerHidden]
    public AnonymousType0(TName name, TPrice price)
    {
        this.name = name;
        this.price = price;
    }

    public TName Name => this.name;

    public TPrice Price => this.price;

    [DebuggerHidden]
    public override bool Equals(object value) =>
        value is AnonymousType0<TName, TPrice> anonymous
        && anonymous != null
        && EqualityComparer<TName>.Default.Equals(this.name, anonymous.name)
        && EqualityComparer<TPrice>.Default.Equals(this.price, anonymous.price);

    // Other members.
}

上記の設定プロパティのような構文は、通常のコンストラクター呼び出しにコンパイルされます:

internal static void CompiledAnonymousType()
{
    AnonymousType0<string, decimal> immutableDevice = new AnonymousType0<string, decimal>(
        name: "Surface Book", price: 1349.00M);
}

コードで使用されている他の異なる匿名型がある場合、C# コンパイラは、より多くの型定義 AnonymousType1、AnonymousType2 などを生成します。匿名型は、プロパティが同じ番号、名前、型、および順序を持つ場合、異なるインスタンス化によって再利用されます:

internal static void ReuseAnonymousType()
{
    var device1 = new { Name = "Surface Book", Price = 1349.00M };
    var device2 = new { Name = "Surface Pro 4", Price = 899.00M };
    var device3 = new { Name = "Xbox One S", Price = 399.00 }; // Price is of type double.
    var device4 = new { Price = 174.99M, Name = "Microsoft Band 2" };
    (device1.GetType() == device2.GetType()).WriteLine(); // True
    (device1.GetType() == device3.GetType()).WriteLine(); // False
    (device1.GetType() == device4.GetType()).WriteLine(); // False
}

匿名型のプロパティ名は、プロパティの初期化に使用された識別子から推測できます。次の 2 つの匿名型のインスタンス化は同等です:

internal static void PropertyInference(Uri uri, int value)
{
    var anonymous1 = new { value, uri.Host };
    var anonymous2 = new { value = value, Host = uri.Host };
}

匿名型は、配列やジェネリック型の型パラメーターなど、他の型の一部にすることもできます:

internal static void AnonymousTypeParameter()
{
    var source = new[] // AnonymousType0<string, decimal>[].
    {
        new { Name = "Surface Book", Price = 1349.00M },
        new { Name = "Surface Pro 4", Price = 899.00M }
    };
    var query = // IEnumerable<AnonymousType0<string, decimal>>.
        source.Where(device => device.Price > 0);
}

ここでは、各配列値が AnonymousType0 型であるため、ソース配列は AnonymousType0[] 型であると推測されます。配列 T[] は IEnumerable インターフェイスを実装しているため、ソース配列は IEnumerable> インターフェイスを実装しています。その Where 拡張メソッドは AnonymousType0 –> bool 述語関数を受け入れ、IEnumerable>.

を返します。

C# コンパイラは、LINQ クエリ式の let 句に匿名型を使用します。 let 句は、匿名型を返すセレクター関数を使用して Select クエリ メソッド呼び出しにコンパイルされます。例:

internal static void Let(IEnumerable<int> source)
{
    IEnumerable<double> query =
        from immutable1 in source
        let immutable2 = Math.Sqrt(immutable1)
        select immutable1 + immutable2;
}

internal static void CompiledLet(IEnumerable<int> source)
{
    IEnumerable<double> query = source // from clause.
        .Select(immutable1 => new { immutable1, immutable2 = Math.Sqrt(immutable1) }) // let clause.
        .Select(anonymous => anonymous.immutable1 + anonymous.immutable2); // select clause.
}

クエリ式のコンパイルの詳細については、LINQ to Objects の章で説明しています。

ローカル変数の型推論

匿名型のローカル変数の他に、var キーワードを使用して既存の型のローカル変数を初期化することもできます:

internal static void LocalVariable(IEnumerable<int> source, string path)
{
    var a = default(int); // int.
    var b = 1M; // decimal.
    var c = typeof(void); // Type.
    var d = from int32 in source where int32 > 0 select Math.Sqrt(int32); // IEnumerable<double>.
    var e = File.ReadAllLines(path); // string[].
}

これは単なるシンタックス シュガーです。ローカル変数の型は、初期値の型から推測されます。暗黙的に型指定されたローカル変数のコンパイルは、明示的に型指定されたローカル変数と違いはありません。初期値の型があいまいな場合、var キーワードを直接使用することはできません:

internal static void LocalVariableWithType()
{
    var f = (Uri)null;
    var g = (Func<int, int>)(int32 => int32 + 1);
    var h = (Expression<Func<int, int>>)(int32 => int32 + 1);
}

一貫性と読みやすさのために、このチュートリアルでは可能な場合は明示的な型指定を使用し、必要な場合は暗黙的な型指定 (var) を使用します (匿名型の場合)。

不変タプルと可変タプル

タプルは、関数型プログラミングで一般的に使用される別の種類のデータ構造です。これは、値の有限で順序付けられたリストであり、通常、ほとんどの関数型言語では不変です。タプルを表すために、.NET Framework 3.5 以降、1 ~ 8 個の型パラメーターを持つ一連のジェネリック タプル クラスが提供されています。たとえば、次は 2 タプル (2 つの値のタプル) を表す Tuple の定義です。

namespace System
{
    [Serializable]
    public class Tuple<T1, T2> : IStructuralEquatable, IStructuralComparable, IComparable, ITuple
    {
        public Tuple(T1 item1, T2 item2)
        {
            this.Item1 = item1;
            this.Item2 = item2;
        }

        public T1 Item1 { get; }

        public T2 Item2 { get; }

        // Other members.
    }
}

すべてのタプル クラスは不変です。最新の C# 7.0 ではタプル構文が導入されています。これは、1 ~ 8 個の型パラメーターを持つ一連の汎用タプル構造で機能します。たとえば、2 タプルは次の ValueTuple 構造体で表されるようになりました:

namespace System
{
    [StructLayout(LayoutKind.Auto)]
    public struct ValueTuple<T1, T2> : IEquatable<ValueTuple<T1, T2>>, IStructuralEquatable, IStructuralComparable, IComparable, IComparable<ValueTuple<T1, T2>>, ITupleInternal
    {
        public T1 Item1;

        public T2 Item2;

        public ValueTuple(T1 item1, T2 item2)
        {
            this.Item1 = item1;
            this.Item2 = item2;
        }

        public override bool Equals(object obj) => obj is ValueTuple<T1, T2> tuple && this.Equals(tuple);

        public bool Equals(ValueTuple<T1, T2> other) =>
            EqualityComparer<T1>.Default.Equals(this.Item1, other.Item1)
            && EqualityComparer<T2>.Default.Equals(this.Item2, other.Item2);

        public int CompareTo(ValueTuple<T1, T2> other)
        {
            int compareItem1 = Comparer<T1>.Default.Compare(this.Item1, other.Item1);
            return compareItem1 != 0 ? compareItem1 : Comparer<T2>.Default.Compare(this.Item2, other.Item2);
        }

        public override string ToString() => $"({this.Item1}, {this.Item2})";

        // Other members.
    }
}

値のタプルは、ヒープ割り当てとガベージ コレクションを管理しないため、パフォーマンスを向上させるために提供されています。ただし、すべての値のタプル構造は変更可能な型になり、値は単なるパブリック フィールドになります。機能と一貫性を保つために、このチュートリアルでは値のタプルのみを使用し、それらを不変の型としてのみ使用します。

上記のタプル定義が示すように、リストとは対照的に、タプルの値はさまざまなタイプにすることができます:

internal static void TupleAndList()
{
    ValueTuple<string, decimal> tuple = new ValueTuple<string, decimal>("Surface Book", 1349M);
    List<string> list = new List<string>() { "Surface Book", "1349M" };
}

タプル型と匿名型は概念的に似ており、どちらも値のリストを返す一連のプロパティです。主な違いは、設計時にタプル型が定義され、匿名型がまだ定義されていないことです。したがって、匿名型 (var) は、期待される型を推測するための初期値を持つローカル変数にのみ使用でき、パラメーターの型、戻り値の型、型引数などとしては使用できません。

internal static ValueTuple<string, decimal> Method(ValueTuple<string, decimal> values)
{
    ValueTuple<string, decimal> variable1;
    ValueTuple<string, decimal> variable2 = default;
    IEnumerable<ValueTuple<string, decimal>> variable3;
    return values;
}

internal static var Method(var values) // Cannot be compiled.
{
    var variable1; // Cannot be compiled.
    var variable2 = default; // Cannot be compiled.
    IEnumerable<var> variable3; // Cannot be compiled.
    return values;
}

構築、要素、および要素の推論

C# 7.0 では、非常に便利なタプル シンタックス シュガーが導入されています。タプル型 ValuTuple は (T1, T2, T3, …) に簡略化でき、タプル構造は new ValueTuple(value1, value2, value3, …) になります。 ) は (value1, value2, value3, …) に簡略化できます:

internal static void TupleTypeLiteral()
{
    (string, decimal) tuple1 = ("Surface Pro 4", 899M);
    // Compiled to: 
    // ValueTuple<string, decimal> tuple1 = new ValueTuple<string, decimal>("Surface Pro 4", 899M);

    (int, bool, (string, decimal)) tuple2 = (1, true, ("Surface Studio", 2999M));
    // ValueTuple<int, bool, ValueTuple<string, decimal>> tuple2 = 
    //    new ValueTuple<int, bool, new ValueTuple<string, decimal>>(1, true, ("Surface Studio", 2999M))
}

どうやら、タプルは他の型と同様に、関数のパラメーター/戻り値の型にすることができます。タプルを関数の戻り値の型として使用する場合、タプル構文により、関数が複数の値を返すことが仮想的に可能になります:

internal static (string, decimal) MethodReturnMultipleValues()
// internal static ValueTuple<string, decimal> MethodReturnMultipleValues()
{
    string returnValue1 = default;
    int returnValue2 = default;

    (string, decimal) Function() => (returnValue1, returnValue2);
    // ValueTuple<string, decimal> Function() => new ValueTuple<string, decimal>(returnValue1, returnValue2);

    Func<(string, decimal)> function = () => (returnValue1, returnValue2);
    // Func<ValueTuple<string, decimal>> function = () => new ValueTuple<string, decimal>(returnValue1, returnValue2);

    return (returnValue1, returnValue2);
}

C# 7.0 では、タプルの要素名も導入されているため、タプル型の各値には、構文 (T1 Name1、T2 Name2、T3 Name3、…) を使用してプロパティのような名前を付けることができ、タプル インスタンスの各値は、名前も構文で与えられます (Name1:value1, Name2, value2, Name3 value3, …)。実際の Item1、Item2、Item3、… フィールド名ではなく、意味のある名前でタプルの値にアクセスできるようにします。これは構文糖衣でもあり、コンパイル時に、すべての要素名が基礎となるフィールドにすべて置き換えられます。

internal static void ElementName()
{
    (string Name, decimal Price) tuple1 = ("Surface Pro 4", 899M);
    tuple1.Name.WriteLine();
    tuple1.Price.WriteLine();
    // Compiled to: 
    // ValueTuple<string, decimal> tuple1 = new ValueTuple<string, decimal>("Surface Pro 4", 899M);
    // TraceExtensions.WriteLine(tuple1.Item1);
    // TraceExtensions.WriteLine(tuple1.Item2)

    (string Name, decimal Price) tuple2 = (ProductNanme: "Surface Book", ProductPrice: 1349M);
    tuple2.Name.WriteLine(); // Element names on the right side are ignore.

    var tuple3 = (Name: "Surface Studio", Price: 2999M);
    tuple3.Name.WriteLine(); // Element names are available through var.

    ValueTuple<string, decimal> tuple4 = (Name: "Xbox One", Price: 179M);
    tuple4.Item1.WriteLine(); // Element names are not available on ValueTuple<T1, T2>.
    tuple4.Item2.WriteLine();

    (string Name, decimal Price) Function((string Name, decimal Price) tuple)
    {
        tuple.Name.WriteLine(); // Parameter element names are available in function.
        return (tuple.Name, tuple.Price - 10M);
    };
    var tuple5 = Function(("Xbox One S", 299M));
    tuple5.Name.WriteLine(); // Return value element names are available through var.
    tuple5.Price.WriteLine();

    Func<(string Name, decimal Price), (string Name, decimal Price)> function = tuple =>
    {
        tuple.Name.WriteLine(); // Parameter element names are available in function.
        return (tuple.Name, tuple.Price - 100M);
    };
    var tuple6 = function(("HoloLens", 3000M));
    tuple5.Name.WriteLine(); // Return value element names are available through var.
    tuple5.Price.WriteLine();
}

匿名型のプロパティの推論と同様に、C# 7.1 では、要素の初期化に使用された識別子からタプルの要素名を推論できます。次の 2 つのタプルは同等です:

internal static void ElementInference(Uri uri, int value)
{
    var tuple1 = (value, uri.Host);
    var tuple2 = (value: value, Host: uri.Host);
}

脱構築

C# 7.0 以降、var キーワードを使用して、タプルを値のリストに分解することもできます。この構文は、タプルで表される複数の値を返す関数で使用すると非常に便利です:

internal static void DeconstructTuple()
{
    (string, decimal) GetProductInfo() => ("HoLoLens", 3000M);
    var (name, price) = GetProductInfo();
    name.WriteLine(); // name is string.
    price.WriteLine(); // price is decimal.
}

この分解シンタックス シュガーは、値が out パラメーターとして定義されているタイプに Deconstruct インスタンスまたは拡張メソッドが定義されている限り、任意のタイプで使用できます。前述のデバイス タイプを例にとると、名前、説明、価格の 3 つのプロパティがあるため、その Deconstruct メソッドは次の 2 つの形式のいずれかになります。

internal partial class Device
{
    internal void Deconstruct(out string name, out string description, out decimal price)
    {
        name = this.Name;
        description = this.Description;
        price = this.Price;
    }
}

internal static class DeviceExtensions
{
    internal static void Deconstruct(this Device device, out string name, out string description, out decimal price)
    {
        name = device.Name;
        description = device.Description;
        price = device.Price;
    }
}

これで、var キーワードはデバイスも破壊できます。これは、Destruct メソッド呼び出しにコンパイルされるだけです:

internal static void DeconstructDevice()
{
    Device GetDevice() => new Device() { Name = "Surface studio", Description = "All-in-one PC.", Price = 2999M };
    var (name, description, price) = GetDevice();
    // Compiled to:
    // string name; string description; decimal price;
    // surfaceStudio.Deconstruct(out name, out description, out price);
    name.WriteLine(); // Surface studio
    description.WriteLine(); // All-in-one PC.
    price.WriteLine(); // 2999
}

破棄

タプルの破棄では、要素は Destruct メソッドの out 変数にコンパイルされるため、out 変数のようにアンダースコアを使用して任意の要素を破棄できます:

internal static void Discard()
{
    Device GetDevice() => new Device() { Name = "Surface studio", Description = "All-in-one PC.", Price = 2999M };
    var (_, _, price1) = GetDevice();
    (_, _, decimal price2) = GetDevice();
}

タプルの割り当て

タプル構文を使用すると、Python や他の言語と同様に、C# でも凝ったタプル代入をサポートできるようになりました。次の例では、1 行のコードで 2 つの変数に 2 つの値を代入し、1 行のコードで 2 つの変数の値を交換しています:

internal static void TupleAssignment(int value1, int value2)
{
    (value1, value2) = (1, 2);
    // Compiled to:
    // value1 = 1; value2 = 2;

    (value1, value2) = (value2, value1);
    // Compiled to:
    // int temp1 = value1; int temp2 = value2;
    // value1 = temp2; value2 = temp1;
}

ループとタプルの代入でフィボナッチ数を計算するのは簡単です:

internal static int Fibonacci(int n)
{
    (int a, int b) = (0, 1);
    for (int i = 0; i < n; i++)
    {
        (a, b) = (b, a + b);
    }
    return a;
}

変数に加えて、タプルの割り当ては、型メンバーなどの他のシナリオでも機能します。次の例では、1 行のコードで 2 つの値を 2 つのプロパティに割り当てます:

internal class ImmutableDevice
{
    internal ImmutableDevice(string name, decimal price) =>
        (this.Name, this.Price) = (name, price);

    internal string Name { get; }

    internal decimal Price { get; }
}

不変性と読み取り専用


不変コレクションと読み取り専用コレクション

Microsoft は、System.Collections.Immutable NuGet パッケージを通じて、ImmutableArray、ImmutableDictionary、ImmutableHashSet、ImmutableList、ImmutableQueue、ImmutableSet、ImmutableStack など。前述のとおり、不変コレクションを変更しようとすると、新しい不変コレクションが作成されます:

internal static void ImmutableCollection()
{
    ImmutableList<int> immutableList1 = ImmutableList.Create(1, 2, 3);
    ImmutableList<int> immutableList2 = immutableList1.Add(4); // Create a new collection.
    object.ReferenceEquals(immutableList1, immutableList2).WriteLine(); // False
}

.NET/Core は、ReadOnlyCollection、ReadOnlyDictionary などの読み取り専用コレクションも提供しますが、これは混乱を招く可能性があります。これらの読み取り専用コレクションは、実際には変更可能なコレクションの単純なラッパーです。コレクションを変更するために使用される Add、Remove などのメソッドを実装および公開しないだけです。それらは不変でもスレッドセーフでもありません。次の例では、可変ソースから不変コレクションと読み取り専用コレクションを作成します。ソースが変更されると、不変コレクションは明らかに変更されませんが、読み取り専用コレクションは変更されます:

internal static void ReadOnlyCollection()
{
    List<int> mutableList = new List<int>() { 1, 2, 3 };
    ImmutableList<int> immutableList = ImmutableList.CreateRange(mutableList);
    ReadOnlyCollection<int> readOnlyCollection = new ReadOnlyCollection<int>(mutableList);
    // ReadOnlyCollection<int> wraps a mutable source, just has no methods like Add, Remove, etc.

    mutableList.Add(4);
    immutableList.Count.WriteLine(); // 3
    readOnlyCollection.Count.WriteLine(); // 4
}