C# 関数型プログラミングの詳細 (1) C# 言語の基礎

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

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

最新バージョン: https://weblogs.asp.net/dixin/functional-csharp-fundamentals

C# 1.0 は 2002 年に最初にリリースされました。その最初の言語仕様で最初に述べられているように、C# は「シンプルで最新のオブジェクト指向でタイプセーフな」汎用プログラミング言語です。現在、C# は 7.2 に進化しています。何年にもわたって、多くの優れた言語機能、特に豊富な関数型プログラミング機能が C# に追加されてきました。現在、C# 言語は生産的でエレガント、命令型で宣言型、オブジェクト指向で機能的です。 .NET Framework、.NET Core、Mono、Xamarin、Unity などのフレームワークにより、Windows、Linux、Mac、iOS、Android などのさまざまなプラットフォームで何百万人もの人々が C# を使用しています。

このチュートリアルは、機能面に焦点を当てた C# 言語向けです。読者は、プログラミングと C# 言語に関する一般的な概念を理解していることを前提としています。この章では、C# 1.0 から 7.x の基本的かつ重要な要素と構文を概説し、初心者レベルの読者だけでなく、最近の C# リリースで導入された新しい構文にまだ慣れていない読者を温めます。その他の高度な機能と概念については、後の章で詳しく説明します。このチュートリアルでは、オブジェクト指向プログラミングの継承、アンセーフ コード内のポインター、他のアンマネージ コードとの相互運用、動的プログラミングなど、関数型プログラミングと LINQ の範囲外のトピックと言語機能については説明しません。

C# この章の機能 他の章の機能 対象外の機能
1.0 クラス
構造
インターフェース
列挙
使用ステートメント
デリゲート
イベント
関数メンバー
参照パラメータ
出力パラメータ
パラメータ配列
foreach ステートメント
継承
ポインター
相互運用
1.1 プラグマ ディレクティブ
1.2 IDisposable の foreach
2.0 静的クラス
部分式
ジェネリック型
Null 許容値型
Null 合体演算子
匿名メソッド
発生器
共分散と反分散
一般的な方法
3.0 自動プロパティ
オブジェクト初期化子
コレクション初期化子
匿名型
暗黙的に型指定されたローカル変数
クエリ式
ラムダ式
延長方法
部分的な方法
4.0 名前付き引数
オプションの引数
一般的な共分散と反分散
動的バインディング
5.0 非同期関数
発信者情報の引数
6.0 プロパティ初期化子
辞書初期化子
Null 伝搬演算子
例外フィルター
文字列補間
オペレーター名
静的インポート
表現体メンバー
catch/finally ブロックで待機
7.0 スロー式
桁区切り
出力変数
タプルと分解
ローカル機能
拡張式本体メンバー
ref リターンとローカル
破棄
一般化された非同期リターン
スロー式
パターンマッチング
7.1 デフォルトのリテラル式 Async Main メソッド
推測されるタプル要素名
7.2 ref 構造
数値リテラルの先頭のアンダースコア
末尾にない名前付き引数
パラメータで
ref readonly リターンとローカル
読み取り専用構造
プライベート保護修飾子

型とメンバー

C# は厳密に型指定されています。 C# では、すべての値に型があります。 C# は、クラス、構造体、列挙型、デリゲート、インターフェイスの 5 種類の型をサポートしています。

クラスは、class キーワードで定義された参照型です。フィールド、プロパティ、メソッド、イベント、演算子、インデクサー、コンストラクター、デストラクター、および入れ子になったクラス、構造体、列挙型、デリゲート、およびインターフェイスの型を持つことができます。クラスは常に System.Object から派生します クラス。

namespace System
{
    public class Object
    {
        public Object();

        public static bool Equals(Object objA, Object objB);

        public static bool ReferenceEquals(Object objA, Object objB);

        public virtual bool Equals(Object obj);

        public virtual int GetHashCode();

        public Type GetType();

        public virtual string ToString();

        // Other members.
    }
}

オブジェクトには、2 つのインスタンスが等しいと見なされるかどうかをテストする静的な Equals メソッド、現在のインスタンスと他のインスタンスが等しいと見なされるかどうかをテストするインスタンスの Equals メソッド、および 2 つのインスタンスが同じインスタンスであるかどうかをテストする静的な ReferenceEquals メソッドがあります。インスタンスをすばやくテストするためのハッシュ コード番号を返すデフォルトのハッシュ関数として GetHashCode メソッドがあります。また、現在のインスタンスの型を返す GetType メソッドと、現在のインスタンスのテキスト表現を返す ToString メソッドもあります。

次の例は、.NET Framework での System.Exception クラスの実装の一部です。クラスとさまざまな種類のメンバーを定義する構文を示します. このクラスは System.ISerializable インターフェイスを実装し、System._Exception クラスを派生させます。クラスを定義するとき、基本クラス System.Object は省略できます。

namespace System
{
    [Serializable]
    public class Exception : ISerializable, _Exception // , System.Object
    {
        internal string _message; // Field.
        
        private Exception _innerException; // Field.

        [OptionalField(VersionAdded = 4)]
        private SafeSerializationManager _safeSerializationManager; // Field.

        public Exception InnerException { get { return this._innerException; } } // Property.

        public Exception(string message, Exception innerException) // Constructor.
        {
            this.Init();
            this._message = message;
            this._innerException = innerException;
        }

        public virtual Exception GetBaseException() // Method.
        {
            Exception innerException = this.InnerException;
            Exception result = this;
            while (innerException != null)
            {
                result = innerException;
                innerException = innerException.InnerException;
            }
            return result;
        }

        protected event EventHandler<SafeSerializationEventArgs> SerializeObjectState // Event.
        {
            add
            {
                this._safeSerializationManager.SerializeObjectState += value;
            }
            remove
            {
                this._safeSerializationManager.SerializeObjectState -= value;
            }
        }

        internal enum ExceptionMessageKind // Nested enumeration type.
        {
            ThreadAbort = 1,
            ThreadInterrupted = 2,
            OutOfMemory = 3
        }

        // Other members.
    }
}

構造体は struct キーワードで定義された値型で、System.Object から派生します。 クラス。デストラクタ以外のクラスのすべての種類のメンバーを持つことができます。構造体は常に System.ValueType から派生します クラスであり、興味深いことに、System.ValueType は System.Object から派生した参照型です。実際には、メモリの割り当て/割り当て解除のパフォーマンスを向上させるために、構造体は通常、非常に小さくて不変のデータ構造を表すように定義されます。たとえば、. .NET Core システム内。次のように実装されています:

namespace System
{
    public struct TimeSpan : IComparable, IComparable<TimeSpan>, IEquatable<TimeSpan>, IFormattable // , System.ValueType
    {
        public const long TicksPerMillisecond = 10000; // Constant.

        public static readonly TimeSpan Zero = new TimeSpan(0); // Field.

        internal long _ticks; // Field.

        public TimeSpan(long ticks) // Constructor.
        {
            this._ticks = ticks;
        }

        public long Ticks { get { return _ticks; } } // Property.

        public int Milliseconds // Property.
        {
            get { return (int)((_ticks / TicksPerMillisecond) % 1000); }
        }

        public static bool Equals(TimeSpan t1, TimeSpan t2) // Method.
        {
            return t1._ticks == t2._ticks;
        }

        public static bool operator ==(TimeSpan t1, TimeSpan t2) // Operator.
        {
            return t1._ticks == t2._ticks;
        }

        // Other members.
    }
}

列挙型は、System.ValueType クラスから派生した System.Enum クラスから派生した値型です。指定された基礎となる整数型 (int) の定数フィールドのみを持つことができます デフォルトで)。例:

namespace System
{
    [Serializable]
    public enum DayOfWeek // : int
    {
        Sunday = 0,
        Monday = 1,
        Tuesday = 2,
        Wednesday = 3,
        Thursday = 4,
        Friday = 5,
        Saturday = 6,
    }
}

デリゲートは System.MulticastDelegate から派生した参照型です System.Delegate から派生したクラス クラス。デリゲート型は関数型を表し、関数型プログラミングの章で詳しく説明されています。

namespace System
{
    public delegate void Action();
}

インターフェイスは、クラスまたは構造体によって実装される契約です。インターフェイスは、実装なしでパブリックおよび抽象プロパティ、メソッド、およびイベントのみを持つことができます。例:

namespace System.ComponentModel
{
    public interface INotifyDataErrorInfo
    {
        event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged; // Event.

        bool HasErrors { get; } // Property.

        IEnumerable GetErrors(string propertyName); // Method.
    }
}

上記のインターフェースを実装するクラスまたは構造体は、指定された 3 つのメンバーをパブリックとして持つ必要があります。

組み込み型

基本あります。 NET 型は C# プログラミングで最も一般的に使用されるため、C# では、C# の組み込み型と呼ばれるこれらの型のエイリアスとして言語キーワードが提供されます。

C# キーワード .NET タイプ
bool System.Boolean
sbyte System.SByte
バイト System.Byte
char System.Char
短い System.Init16
ushort System.UInit16
int System.Init32
uint System.UInit32
長い System.Init54
ulong System.UInit54
フロート System.Single
ダブル System.Double
10 進数 System.Decimal
オブジェクト System.Object
文字列 System.String

参照型と値型

C#/.NET では、クラスはオブジェクト、文字列、配列などを含む参照型です。デリゲートも参照型です。これについては後で説明します。構造体は、プリミティブ型 (bool) を含む値型です。 、sbyteバイト , 文字短いushort , 整数単位長いロングフロートダブル )、小数System.DateTimeSystem.DateTimeOffsetSystem.TimeSpanSystem.GuidSystem.Nullable 、列挙型 (列挙型の基になる型は常に数値プリミティブ型であるため) など。次の例では、互いに類似した参照型と値型を定義しています。

internal class Point
{
    private readonly int x;

    private readonly int y;

    internal Point(int x, int y)
    {
        this.x = x;
        this.y = y;
    }

    internal int X { get { return this.x; } }

    internal int Y { get { return this.y; } }
}

internal readonly struct ValuePoint
{
    private readonly int x;

    private readonly int y;

    internal ValuePoint(int x, int y)
    {
        this.x = x;
        this.y = y;
    }

    internal int X { get { return this.x; } }

    internal int Y { get { return this.y; } }
}

参照型と値型のインスタンスは別々に割り当てられます。参照型は常にマネージド ヒープに割り当てられ、ガベージ コレクションによって割り当て解除されます。値の型は、スタックに割り当てられ、スタックの巻き戻しによって割り当てが解除されるか、コンテナーにインラインで割り当てられ、割り当てが解除されます。そのため、一般的に、値型は割り当てと割り当て解除のパフォーマンスが向上する可能性があります。通常、型が小さく、不変で、プリミティブ型と論理的に類似している場合、その型は値型として設計できます。上記の System.TimeSpan 型構造は期間を表します。これは、ティックを表す long 値の不変ラッパーであるため、値型になるように設計されています。次の例は、この違いを示しています:

internal static partial class Fundamentals
{
    internal static void ValueTypeReferenceType()
    {
        Point reference1 = new Point(1, 2);
        Point reference2 = reference1;
        Trace.WriteLine(object.ReferenceEquals(reference1, reference2)); // True

        ValuePoint value1 = new ValuePoint(3, 4);
        ValuePoint value2 = value1;
        Trace.WriteLine(object.ReferenceEquals(value1, value2)); // False

        Point[] referenceArray = new Point[] { new Point(5, 6) };
        ValuePoint[] valueArray = new ValuePoint[] { new ValuePoint(7, 8) };
    }
}

ポイント インスタンスはローカル変数として構築され、参照型であるため、マネージド ヒープに割り当てられます。そのフィールドは値型であるため、フィールドはマネージ ヒープにもインラインで割り当てられます。ローカル変数 reference1 データを保持するマネージ ヒープの場所を指すポインターとして表示できます。 reference1 を割り当てる場合 リファレンス 2 へ 、ポインターがコピーされます。だから reference1reference2 どちらも同じ Point を指しています マネージド ヒープ内のインスタンス。 ValuePoint の場合 値型であるため、ローカル変数として構築されます。スタックに割り当てられます。そのフィールドもスタックにインラインで割り当てられます。ローカル変数 value1 実際のデータを保持します。 value2 に value1 を代入する場合 、インスタンス全体がコピーされるため、value1value2 2 つの異なる ValuePoint です スタック内のインスタンス。 C# では、配列は常に System.Array クラスから派生し、参照型です。したがって、referenceArray と valueArray は両方ともヒープ上に割り当てられ、それらの要素も両方ともヒープ上にあります。

参照型は null にすることができ、値型はできません:

internal static void Default()
{
    Point defaultReference = default(Point);
    Trace.WriteLine(defaultReference is null); // True

    ValuePoint defaultValue = default(ValuePoint);
    Trace.WriteLine(defaultValue.X); // 0
    Trace.WriteLine(defaultValue.Y); // 0
}

参照型のデフォルト値は単純に null です。値の型のデフォルトは、すべてのフィールドがデフォルト値に初期化された実際のインスタンスです。実際、上記のローカル変数の初期化は次のようにコンパイルされます:

internal static void CompiledDefault()
{
    Point defaultReference = null;

    ValuePoint defaultValue = new ValuePoint();
}

構造体には常に、実質的にパラメーターなしの既定のコンストラクターがあります。この既定のコンストラクターを呼び出すと、構造体がインスタンス化され、そのすべてのフィールドが既定値に設定されます。ここに defaultValueint フィールドは 0 に初期化されます。ValuePoint の場合 参照型フィールドがある場合、参照型フィールドは null に初期化されます。

デフォルトのリテラル式

C# 7.1 以降、型が推測できる場合は、既定値式の型を省略できます。したがって、上記のデフォルト値の構文は次のように簡略化できます:

internal static void DefaultLiteralExpression()
{
    Point defaultReference = default;

    ValuePoint defaultValue = default;
}

参照構造

C# 7.2 では、構造体定義の ref キーワードが有効になっているため、構造体をスタックにのみ割り当てることができます。これは、ヒープでのメモリの割り当て/割り当て解除がパフォーマンスのオーバーヘッドになる可能性がある、パフォーマンスが重要なシナリオで役立ちます。

internal ref struct OnStackOnly { }

internal static void Allocation()
{
    OnStackOnly valueOnStack = new OnStackOnly();
    OnStackOnly[] arrayOnHeap = new OnStackOnly[10]; // Cannot be compiled.
}

internal class OnHeapOnly
{
    private OnStackOnly fieldOnHeap; // Cannot be compiled.
}

internal struct OnStackOrHeap
{
    private OnStackOnly fieldOnStackOrHeap; // Cannot be compiled.
}

前述のように、配列はヒープに割り当てられた参照型であるため、コンパイラは ref 構造体の配列を許可しません。クラスのインスタンスは常にヒープ上でインスタンス化されるため、ref 構造体をそのフィールドとして使用することはできません。通常の構造体のインスタンスはスタックまたはヒープ上にある可能性があるため、ref 構造体をそのフィールドとして使用することもできません。

静的クラス

C# 2.0 は 静的 を有効にします クラス定義の修飾子。 System.Math を例に取ります:

namespace System
{
    public static class Math
    {
        // Static members only.
    }
}

静的クラスは静的メンバーのみを持つことができ、インスタンス化することはできません。静的クラスは、抽象化されたシール クラスにコンパイルされます。 C# では、一連の静的メソッドをホストするために static がよく使用されます。

部分型

C# 2.0 では partial が導入されました 設計時にクラス、構造体、またはインターフェースの定義を分割するためのキーワード。

internal partial class Device
{
    private string name;

    internal string Name
    {
        get { return this.name; }
        set { this.name = value; }
    }
}

internal partial class Device
{
    public string FormattedName
    {
        get { return this.name.ToUpper(); }
    }
}

これは、大きなタイプを複数の小さなファイルに分割して管理するのに適しています。部分型はコード生成でも頻繁に使用されるため、ユーザーはツールによって生成された型にカスタム コードを追加できます。コンパイル時に、型の複数の部分がマージされます。

インターフェースと実装

型がインターフェイスを実装する場合、この型は各インターフェイス メンバーを暗黙的または明示的に実装できます。次のインターフェイスには 2 つのメンバー メソッドがあります:

internal interface IInterface
{
    void Implicit();

    void Explicit();
}

そして、このインターフェースを実装する次の型:

internal class Implementation : IInterface
{
    public void Implicit() { }

    void IInterface.Explicit() { }
}

この実装 型には public Implicit があります IInterface と同じシグネチャを持つメソッド の暗黙的 メソッドであるため、C# コンパイラは 実装 を取ります。 IInterface の実装としての暗黙的なメソッド。 暗黙的な方法。この構文は、暗黙的なインターフェイスの実装と呼ばれます。もう 1 つのメソッド Explicit は、実装型のメンバー メソッドとしてではなく、インターフェイス メンバーとして明示的に実装されます。次の例は、これらのインターフェイス メンバーの使用方法を示しています。

internal static void InterfaceMembers()
{
    Implementation @object = new Implementation();
    @object.Implicit(); // @object.Explicit(); cannot be compiled.

    IInterface @interface = @object;
    @interface.Implicit();
    @interface.Explicit();
}

暗黙的に実装されたインターフェイス メンバーは、実装型とインターフェイス型のインスタンスからアクセスできますが、明示的に実装されたインターフェイス メンバーは、インターフェイス型のインスタンスからのみアクセスできます。ここで変数名 @object@interface object であるため、特殊文字 @ の接頭辞が付きます とインターフェース は C# 言語のキーワードであり、識別子として直接使用することはできません。

IDisposable インターフェイスと using ステートメント

実行時に、CLR/CoreCLR はメモリを自動的に管理します。 .NET オブジェクトにメモリを割り当て、ガベージ コレクタでメモリを解放します。 .NET オブジェクトは、開いているファイル、ウィンドウ ハンドル、データベース接続など、CLR/CoreCLR によって管理されていない他のリソースを割り当てることもできます。.NET は、これらの型の標準契約を提供します:

namespace System
{
    public interface IDisposable
    {
        void Dispose();
    }
}

上記の System.IDisposable インターフェイスを実装する型には、呼び出されたときにアンマネージ リソースを明示的に解放する Dispose メソッドが必要です。たとえば、System.Data.SqlClient.SqlConnection は SQL データベースへの接続を表し、IDisposable を実装し、基礎となるデータベース接続を解放する Dispose メソッドを提供します。次の例は、IDisposable オブジェクトを使用して Dispose メソッドを呼び出す標準の try-finally パターンです:

internal static void Dispose(string connectionString)
{
    SqlConnection connection = new SqlConnection(connectionString);
    try
    {
        connection.Open();
        Trace.WriteLine(connection.ServerVersion);
        // Work with connection.
    }
    finally
    {
        if ((object)connection != null)
        {
            ((IDisposable)connection).Dispose();
        }
    }
}

Dispose メソッドは finally ブロックで呼び出されるため、try ブロックの操作から例外がスローされた場合や、現在のスレッドが中止された場合でも、確実に呼び出されます。 IDisposable は広く使用されているため、C# は 1.0 以降、using ステートメントのシンタックス シュガーを導入しています。上記のコードは次と同等です:

internal static void Using(string connectionString)
{
    using (SqlConnection connection = new SqlConnection(connectionString))
    {
        connection.Open();
        Trace.WriteLine(connection.ServerVersion);
        // Work with connection.
    }
}

これは、設計時にはより宣言的であり、try-finally はコンパイル時に生成されます。 Dispose メソッドが正しい方法で呼び出されるように、使い捨て可能なインスタンスは常にこの構文で使用する必要があります。

ジェネリック型

C# 2.0 では、ジェネリック プログラミングが導入されています。ジェネリック プログラミングは、型パラメーターをサポートするパラダイムであるため、型情報を後で提供できます。次の int のスタック データ構造 値は一般的ではありません:

internal interface IInt32Stack
{
    void Push(int value);

    int Pop();
}

internal class Int32Stack : IInt32Stack
{
    private int[] values = new int[0];

    public void Push(int value)
    {
        Array.Resize(ref this.values, this.values.Length + 1);
        this.values[this.values.Length - 1] = value;
    }

    public int Pop()
    {
        if (this.values.Length == 0)
        {
            throw new InvalidOperationException("Stack empty.");
        }
        int value = this.values[this.values.Length - 1];
        Array.Resize(ref this.values, this.values.Length - 1);
        return value;
    }
}

このコードはあまり再利用できません。後で、文字列や小数など、他のデータ型の値にスタックが必要な場合は、いくつかのオプションがあります:

  • 新しいデータ型ごとに、上記のコードのコピーを作成し、int 型情報を変更します。だから IStringStackStringStack string に対して定義できます , IDecimalStack および DecimalStack 小数 などなど。どうやらこの方法は実現不可能です。
  • すべての型は オブジェクト から派生するため 、オブジェクトの一般的なスタック IObjectStack で定義できます と ObjectStack . プッシュ メソッドはオブジェクトを受け入れます 、ポップ メソッドはオブジェクトを返します であるため、スタックは任意のデータ型の値に使用できます。ただし、この設計ではコンパイル時の型チェックが失われます。 プッシュを呼び出す 任意の引数でコンパイルできます。また、実行時に Pop するたびに が呼び出された場合、返されたオブジェクトを期待される型にキャストする必要があります。これは、パフォーマンスのオーバーヘッドであり、失敗する可能性があります。

型パラメータ

ジェネリックでは、具体的な型 int を型パラメーター T に置き換えることをお勧めします。これは、スタック型名に続く山かっこで宣言されます。

internal interface IStack<T>
{
    void Push(T value);

    T Pop();
}

internal class Stack<T> : IStack<T>
{
    private T[] values = new T[0];

    public void Push(T value)
    {
        Array.Resize(ref this.values, this.values.Length + 1);
        this.values[this.values.Length - 1] = value;
    }

    public T Pop()
    {
        if (this.values.Length == 0)
        {
            throw new InvalidOperationException("Stack empty.");
        }
        T value = this.values[this.values.Length - 1];
        Array.Resize(ref this.values, this.values.Length - 1);
        return value;
    }
}

このジェネリック スタックを使用する場合は、パラメータ T:の具象型を指定します。

internal static void Stack()
{
    Stack<int> stack1 = new Stack<int>();
    stack1.Push(int.MaxValue);
    int value1 = stack1.Pop();

    Stack<string> stack2 = new Stack<string>();
    stack2.Push(Environment.MachineName);
    string value2 = stack2.Pop();

    Stack<Uri> stack3 = new Stack<Uri>();
    stack3.Push(new Uri("https://weblogs.asp.net/dixin"));
    Uri value3 = stack3.Pop();
}

そのため、ジェネリクスにより、タイプ セーフなコードの再利用が可能になります。 IStackスタック IStack. の強い型付けです。 プッシュ /Stack.Push タイプ T の値を受け入れる 、および IStack ポップ /IStack.Pop タイプ T の値を返します .たとえば、T の場合 整数です 、IStack .押す /Stack.Push int を受け入れる 価値; T の場合 文字列です , IStack.Pop /Stack.Pop 文字列を返します 価値;など。だから IStackスタック はポリモーフィック型であり、これはパラメトリック ポリモーフィズムと呼ばれます。

.NET では、型パラメーターを持つジェネリック型は、オープン型 (またはオープン構築型) と呼ばれます。ジェネリック型のすべての型パラメーターが具体的な型で指定されている場合、それは閉じた型 (または閉じた構築された型) と呼ばれます。ここでスタック オープンタイプで、Stackスタックスタック

ジェネリック構造体の構文は、上記のジェネリック クラスと同じです。ジェネリック デリゲートとジェネリック メソッドについては後で説明します。

型パラメータの制約

上記のジェネリック型と次のジェネリック型の場合、型パラメーターは任意の値にすることができます:

internal class Constraint<T>
{
    internal void Method()
    {
        T value = null;
    }
}

上記のコードはコンパイルできず、エラー CS0403:Cannot convert null to type parameter 'T' because it could be a non-nullable value type.その理由は、前述のように、参照型 (クラスのインスタンス) の値のみが null になることができるためです。 、しかしここでは T 構造型も許可されます。この種のシナリオでは、C# は where キーワードを使用した型パラメーターの制約をサポートしています:

internal class Constraint<T> where T: class
{
    internal static void Method()
    {
        T value1 = null;
    }
}

ここで、T は参照型である必要があります。たとえば、Constraint です。 コンパイラによって許可され、Constraint コンパイラ エラーが発生します。制約構文のその他の例を次に示します:

internal partial class Constraints<T1, T2, T3, T4, T5, T6, T7>
    where T1 : struct
    where T2 : class
    where T3 : DbConnection
    where T4 : IDisposable
    where T5 : struct, IComparable, IComparable<T5>
    where T6 : new()
    where T7 : T2, T3, T4, IDisposable, new() { }

上記のジェネリック型には 7 つの型パラメーターがあります:

  • T1 値型 (構造体) である必要があります
  • T2 参照型 (クラス) である必要があります
  • T3 指定された型であるか、指定された型から派生している必要があります
  • T4 指定されたインターフェースであるか、指定されたインターフェースを実装する必要があります
  • T5 値型 (構造体) である必要があり、指定されたすべてのインターフェイスを実装する必要があります
  • T6 パラメーターなしの public コンストラクターが必要です
  • T7 T2 である、または T2 から派生するか、実装する必要があります 、T3T4 、指定されたインターフェースを実装する必要があり、パブリックのパラメーターなしのコンストラクターが必要です

T3 を取る 例:

internal partial class Constraints<T1, T2, T3, T4, T5, T6, T7>
{
    internal static void Method(T3 connection)
    {
        using (connection) // DbConnection implements IDisposable.
        {
            connection.Open(); // DbConnection has Open method.
        }
    }
}

System.Data.Common.DbConnection について System.IDisposable を実装します 、および CreateCommand を持っています メソッドなので、上記の t3 オブジェクトは using ステートメントで使用でき、CreateCommand call もコンパイルできます。

以下は、Constraints の閉じた型の例です。 :

internal static void CloseType()
{
    Constraints<bool, object, DbConnection, IDbConnection, int, Exception, SqlConnection> closed = default;
}

ここ:

  • bool は値型です
  • オブジェクトは参照型です
  • DbConnection は DbConnection です
  • System.Data.Common.IDbConnection は IDisposable を実装します
  • int は値型で、System.IComparable を実装し、System.IComparable も実装します
  • System.Exception には、パラメーターなしの public コンストラクターがあります
  • System.Data.SqlClient.SqlConnection はオブジェクトから派生し、DbConnection から派生し、IDbConnection を実装し、パラメーターなしのパブリック コンストラクターを持ちます

NULL 値型

前述のとおり、C#/.NET では、型のインスタンスを null にすることはできません。ただし、値の型が論理的な null を表すシナリオがまだいくつかあります。典型的な例はデータベーステーブルです。 null 許容整数列から取得される値は、整数値または null のいずれかです。 C# 2.0 では、int? などの null 許容値型の構文 T? が導入されています。 null 許容 int を読み取ります。た? System.Nullable ジェネリック構造の単なるショートカットです:

namespace System
{
    public struct Nullable<T> where T : struct
    {
        private bool hasValue;

        internal T value;

        public Nullable(T value)
        {
            this.value = value;
            this.hasValue = true;
        }

        public bool HasValue
        {
            get { return this.hasValue; }
        }

        public T Value
        {
            get
            {
                if (!this.hasValue)
                {
                    throw new InvalidOperationException("Nullable object must have a value.");
                }
                return this.value;
            }
        }

        // Other members.
    }
}

次の例は、null 許容 int の使用方法を示しています:

internal static void Nullable()
{
    int? nullable = null;
    nullable = 1;
    if (nullable != null)
    {
        int value = (int)nullable;
    }
}

どうやら、int? Nullable 構造であり、実際の null にすることはできません。上記のコードはシンタックス シュガーであり、通常の構造体の使用法にコンパイルされています:

internal static void CompiledNullable()
{
    Nullable<int> nullable = new Nullable<int>();
    nullable = new Nullable<int>(1);
    if (nullable.HasValue)
    {
        int value = nullable.Value;
    }
}

nullable に null を代入すると、実際には Nullable インスタンスのインスタンスが代入されます。ここでは、構造体の既定のパラメーターなしのコンストラクターが呼び出されるため、Nullable インスタンスが初期化され、各データ フィールドが既定値で初期化されます。したがって、nullable の hasValue フィールドは false であり、このインスタンスが論理的に null を表していることを示します。次に、nullable が通常の int 値で再割り当てされ、実際には別の Nullable インスタンスが割り当てられます。このインスタンスでは、hasValue フィールドが true に設定され、value フィールドが指定された int 値に設定されます。 null 以外のチェックは、HasValue プロパティ呼び出しにコンパイルされます。そしてintからの型変換? to int は Value プロパティの呼び出しにコンパイルされます。

自動プロパティ

プロパティは基本的に、本体を持つゲッターおよび/または本体を持つセッターです。多くの場合、プロパティのセッターとゲッターは、上記のデバイス タイプの Name プロパティのように、データ フィールドをラップするだけです。型にデータ フィールドをラップするためのプロパティが多数ある場合、このパターンは煩わしい場合があるため、C# 3.0 では自動プロパティのシンタックス シュガーが導入されています。

internal partial class Device
{
    internal decimal Price { get; set; }
}

バッキング フィールドの定義と getter/setter の本体は、コンパイラによって生成されます:

internal class CompiledDevice
{
    [CompilerGenerated]
    private decimal priceBackingField;

    internal decimal Price
    {
        [CompilerGenerated]
        get { return this.priceBackingField; }

        [CompilerGenerated]
        set { this.priceBackingField = value; }
    }

    // Other members.
}

C# 6.0 以降、auto プロパティは getter のみにできます:

internal partial class Category
{
    internal Category(string name)
    {
        this.Name = name;
    }

    internal string Name { get; }
}

上記の Name プロパティは getter のみを持つようにコンパイルされ、バッキング フィールドは読み取り専用になります:

internal partial class CompiledCategory
{
    [CompilerGenerated]
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private readonly string nameBackingField;

    internal CompiledCategory(string name)
    {
        this.nameBackingField = name;
    }

    internal string Name
    {
        [CompilerGenerated]
        get { return this.nameBackingField; }
    }
}

プロパティ初期化子

C# 6.0 ではプロパティ初期化子のシンタックス シュガーが導入され、プロパティの初期値をインラインで提供できるようになりました。

internal partial class Category
{
    internal Guid Id { get; } = Guid.NewGuid();

    internal string Description { get; set; } = string.Empty;
}

プロパティ初期化子はバッキング フィールド初期化子にコンパイルされます:

internal partial class CompiledCategory
{
    [CompilerGenerated]
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private readonly Guid idBackingField = Guid.NewGuid();

    [CompilerGenerated]
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private string descriptionBackingField = string.Empty;

    internal Guid Id
    {
        [CompilerGenerated]
        get { return this.idBackingField; }
    }

    internal string Description
    {
        [CompilerGenerated]
        get { return this.descriptionBackingField; }

        [CompilerGenerated]
        set { this.descriptionBackingField = value; }
    }
}

オブジェクト初期化子

Device インスタンスは、一連の命令型プロパティ割り当てステートメントで初期化できます:

internal static void SetProperties()
{
    Device device = new Device();
    device.Name = "Surface Book";
    device.Price = 1349M;
}

C# 3.0 では、オブジェクト初期化子のシンタックス シュガーが導入されています。呼び出しコンストラクターと設定プロパティ コードの上に、宣言型スタイルでマージできます。

internal static void ObjectInitializer()
{
    Device device = new Device() { Name = "Surface Book", Price = 1349M };
}

2 番目の例のオブジェクト初期化子の構文は、最初の例の一連の代入にコンパイルされます。

コレクション初期化子

同様に、C# 3.0 では、System.Collections.IEnumerable インターフェイスを実装し、パラメーター化された Add メソッドを持つ型のコレクション初期化子のシンタックス シュガーも導入されています。次のデバイス コレクションを例に取ります:

internal class DeviceCollection : IEnumerable
{
    private Device[] devices = new Device[0];

    internal void Add(Device device)
    {
        Array.Resize(ref this.devices, this.devices.Length + 1);
        this.devices[this.devices.Length - 1] = device;
    }

    public IEnumerator GetEnumerator() // From IEnumerable.
    {
        return this.devices.GetEnumerator();
    }
}

宣言的に初期化することもできます:

internal static void CollectionInitializer(Device device1, Device device2)
{
    DeviceCollection devices = new DeviceCollection() { device1, device2 };
}

上記のコードは、通常のコンストラクター呼び出しにコンパイルされ、その後に一連の Add メソッド呼び出しが続きます。

internal static void CompiledCollectionInitializer(Device device1, Device device2)
{
    DeviceCollection devices = new DeviceCollection();
    devices.Add(device1);
    devices.Add(device2);
}

インデックス初期化子

C# 6.0 では、インデクサー セッターを使用した型のインデックス初期化子が導入されています:

internal class DeviceDictionary
{
    internal Device this[int id] { set { } }
}

これは別の宣言型構文糖衣です:

internal static void IndexInitializer(Device device1, Device device2)
{
    DeviceDictionary devices = new DeviceDictionary { [10] = device1, [11] = device2 };
}

上記の構文は、通常のコンストラクター呼び出しにコンパイルされ、その後に一連のインデクサー呼び出しが続きます。

internal static void CompiledIndexInitializer(Device device1, Device device2)
{
    DeviceDictionary devices = new DeviceDictionary();
    devices[0] = device1;
    devices[1] = device2;
}

NULL 合体演算子

C# 2.0 では、null 合体演算子 ?? が導入されています。左のように2オペランドで動作しますか??右。左のオペランドが null でない場合は左のオペランドを返し、それ以外の場合は右のオペランドを返します。たとえば、参照または null 許容値を操作する場合、実行時に null チェックを行い、null を置き換えることは非常に一般的です。

internal partial class Point
{
    internal static Point Default { get; } = new Point(0, 0);
}

internal partial struct ValuePoint
{
    internal static ValuePoint Default { get; } = new ValuePoint(0, 0);
}

internal static void DefaultValueForNull(Point reference, ValuePoint? nullableValue)
{
    Point point = reference != null ? reference : Point.Default;

    ValuePoint valuePoint = nullableValue != null ? (ValuePoint)nullableValue : ValuePoint.Default;
}

これは、null 合体演算子で簡略化できます:

internal static void DefaultValueForNullWithNullCoalescing(Point reference, ValuePoint? nullableValue)
{
    Point point = reference ?? Point.Default;
    ValuePoint valuePoint = nullableValue ?? ValuePoint.Default;
}

NULL 条件演算子

メンバーまたはインデクサー アクセスの前に null をチェックすることも非常に一般的です。

internal static void NullCheck(Category category, Device[] devices)
{
    string categoryText = null;
    if (category != null)
    {
        categoryText = category.ToString();
    }
    string firstDeviceName;
    if (devices != null)
    {
        Device firstDevice = devices[0];
        if (first != null)
        {
            firstDeviceName = firstDevice.Name;
        }
    }
}

C# 6.0 では、null 条件演算子 (null 伝搬演算子とも呼ばれる) ? が導入されました。これを簡素化するために、メンバー アクセスには ?[] を、インデクサー アクセスには ?[] を使用します。

internal static void NullCheckWithNullConditional(Category category, Device[] devices)
{
    string categoryText = category?.ToString();
    string firstDeviceName = devices?[0]?.Name;
}

スロー式

C# 7.0 から、throw ステートメントを式として使用できるようになりました。 throw 式は、引数チェックを簡素化するために、条件演算子および null 合体演算子の上で頻繁に使用されます。

internal partial class Subcategory
{
    internal Subcategory(string name, Category category)
    {
        this.Name = !string.IsNullOrWhiteSpace(name) ? name : throw new ArgumentNullException("name");
        this.Category = category ?? throw new ArgumentNullException("category");
    }

    internal Category Category { get; }

    internal string Name { get; }
}

例外フィルター

C# では、例外をキャッチし、フィルター処理してから処理/再スローするのが一般的でした。次の例では、指定された URI から HTML 文字列をダウンロードしようとします。不正な要求の応答ステータスがある場合、ダウンロードの失敗を処理できます。したがって、チェックする例外をキャッチします。例外に期待される情報がある場合は、例外が処理されます。それ以外の場合は、例外を再スローします。

internal static void CatchFilterRethrow(WebClient webClient)
{
    try
    {
        string html = webClient.DownloadString("http://weblogs.asp.net/dixin");
    }
    catch (WebException exception)
    {
        if ((exception.Response as HttpWebResponse)?.StatusCode == HttpStatusCode.BadRequest)
        {
            // Handle exception.
        }
        else
        {
            throw;
        }
    }
}

C# 6.0 では、言語レベルで例外フィルターが導入されています。 catch ブロックには、キャッチする前に指定された例外をフィルター処理する式を含めることができます。式が true を返す場合、catch ブロックが実行されます:

internal static void ExceptionFilter(WebClient webClient)
{
    try
    {
        string html = webClient.DownloadString("http://weblogs.asp.net/dixin");
    }
    catch (WebException exception) when ((exception.Response as HttpWebResponse)?.StatusCode == HttpStatusCode.BadRequest)
    {
        // Handle exception.
    }
}

例外フィルターはシンタックス シュガーではなく、CLR 機能です。上記の例外フィルター式は、CIL のフィルター句にコンパイルされます。次のクリーンな CIL は、コンパイル結果を仮想的に示しています:

.method assembly hidebysig static void ExceptionFilter(class [System]System.Net.WebClient webClient) cil managed
{
  .try
  {
    // string html = webClient.DownloadString("http://weblogs.asp.net/dixin");
  }
  filter
  {
    // when ((exception.Response as HttpWebResponse)?.StatusCode == HttpStatusCode.BadRequest)
  }
  {
    // Handle exception.
  }
}

フィルター式が false を返す場合、catch 句は実行されないため、例外を再スローする必要はありません。例外を再スローすると、例外が throw ステートメントからのものであるかのように、スタックの巻き戻しが発生し、元の呼び出しスタックとその他の情報が失われます。したがって、この機能は診断とデバッグに非常に役立ちます。

文字列補間

長年にわたり、C# では文字列複合書式が広く使用されています。文字列形式のインデックス付きプレースホルダーに値を挿入します:

internal static void Log(Device device)
{
    string message = string.Format("{0}: {1}, {2}", DateTime.Now.ToString("o"), device.Name, device.Price);
    Trace.WriteLine(message);
}

C# 6.0 では、順序を個別に維持することなく、値を所定の位置で宣言するための文字列補間構文シュガーが導入されています。

internal static void LogWithStringInterpolation(Device device)
{
    string message = string.Format($"{DateTime.Now.ToString("o")}: {device.Name}, {device.Price}");
    Trace.WriteLine(message);
}

2 番目の補間バージョンは、一連のインデックスを維持することなく、より宣言的で生産的です。この構文は、実際には最初の複合フォーマットにコンパイルされます。

オペレーター名

C# 6.0 では、nameof 演算子が導入され、変数、型、またはメンバーの文字列名が取得されます。引数チェックを例に取ります:

internal static void ArgumentCheck(int count)
{
    if (count < 0)
    {
        throw new ArgumentOutOfRangeException("count");
    }
}

パラメータ名はハードコードされた文字列であり、コンパイラでチェックできません。 nameof 演算子を使用すると、コンパイラは上記のパラメーター名の文字列定数を生成できます。

internal static void NameOf(int count)
{
    if (count < 0)
    {
        throw new ArgumentOutOfRangeException(nameof(count));
    }
}

桁区切りと先頭のアンダースコア

C# 7.0 では、桁区切り記号としてアンダースコアが導入され、2 進数の 0b プレフィックスが導入されました。 C# 7.1 は、数字の先頭でオプションのアンダースコアをサポートしています。

internal static void DigitSeparator()
{
    int value1 = 10_000_000;
    double value2 = 0.123_456_789;

    int value3 = 0b0001_0000; // Binary.
    int value4 = 0b_0000_1000; // Binary.
}

これらの小さな機能により、設計時の長い数値と 2 進数の読みやすさが大幅に向上します。

まとめ

この章では、C# の基本的かつ重要な知識 (参照型、値型、ジェネリック型、null 許容値型、初期化子、演算子、式などの基本的な構文など) について説明します。これには、C# の最近のリリースで導入された新しい構文も含まれます。これらの基本に慣れた読者は、C# 言語、関数型プログラミング、および LINQ の他の高度なトピックに飛び込む準備ができているはずです。