C Sharpの基礎 - 拡張メソッド

提供:MochiuWiki - SUSE, Electronic Circuit, PCB
ナビゲーションに移動 検索に移動

概要

C#の拡張メソッドは、既存のクラスやインターフェースに新しいメソッドを追加する機能である。
この機能により、元のソースコードを変更せずに、既存の型に機能を追加することができる。

また、ソースコードにアクセスできない型やシールされた型に対しても機能を拡張することができる。

拡張メソッドの特徴を以下に示す。

  • 元のクラスを変更せずに新しいメソッドを追加できる。
  • インスタンスメソッドのように呼び出せる。
  • 名前空間を使用して拡張メソッドの可視性を制御できる。
  • 継承やインターフェースの実装よりも柔軟である。


ただし、制限事項があることに注意する。

  • 拡張メソッドは既存のメソッドをオーバーライドできない。
  • 拡張メソッド内からプライベートメンバにアクセスできない。


拡張メソッドの主な用途を以下に示す。

  • ユーティリティ関数の追加
  • LINQ (Language Integrated Query) の実装
  • ソースコードの可読性と再利用性の向上


※注意
過剰な使用は避け、適切な場合にのみ使用すること。
名前の衝突に注意が必要である。

 // 例 :
 
 public static class StringExtensions
 {
    public static int WordCount(this string str)
    {
       return str.Split(new char[] { ' ', '.', '?' }, 
                        StringSplitOptions.RemoveEmptyEntries).Length;
    }
 }
 
 // 使用方法
 string text = "Hello, world! How are you?";
 int count = text.WordCount();  // 5



拡張メソッドの実装

  1. スタティッククラスの作成
    拡張メソッドはスタティッククラスの中で定義される。
    これは、拡張メソッドがどのクラスにも属さないため、スタティッククラスが適しているためである。
    また、スタティッククラスは、クライアントコードから参照できる必要がある。

  2. スタティックメソッドの作成
    拡張メソッドはスタティックメソッドとして定義される。
    最初のパラメータ (第1引数) の型名の前には、必ず、thisキーワードを用いて対象の型を指定する。
    これにより、その型のインスタンスでメソッドを呼び出すことが可能になる。

    このパラメータは、レシーバーパラメータとも呼ばれ、拡張メソッドがどの型に対して定義されるかを決定する。

  3. 拡張メソッドの使用
    拡張メソッドは、通常のインスタンスメソッドと同様に呼び出すことができる。
    ただし、異なる名前空間に定義している場合は、usingディレクティブを使用して、拡張メソッドが定義されている名前空間をインポートする必要がある。


もし、拡張メソッド名が既存のインスタンスメソッドと同じ名前の場合、呼び出し元のクラスで定義されているメソッドが優先されるため、
拡張メソッドが使用されることはない。


制約

制約とは

制約 (Constraints) とは、ジェネリック型パラメータに対して特定の条件を課すものである。
これにより、型の安全性を高め、ソースコード内でその型パラメータの特定の機能を使用できるようになる。

※注意
過度に制約を課すと、ジェネリックの柔軟性が失われる可能性がある。
制約は必要最小限にとどめ、コードの再利用性とのバランスを取ることが重要である。
一部の制約の組み合わせは論理的に矛盾する場合がある (例: where T : struct, new())

制約を適切に使用することで、型安全性の高いより表現力豊かなジェネリックコードを記述することができる。
ただし、制約の選択には慎重を期し、設計の目的と要件に基づいて適切に判断することが重要である。

シンタックス

制約には複数の種類があり、カンマ区切りで複数指定することができる。

 where <型パラメ> : <制約1>, <制約2>, <制約3>, ...


主な制約の種類

  • クラス制約 (where T : <クラス名またはサブクラス名>)
    Tは指定されたクラスまたはそのサブクラスである必要がある。
    例: where T : Animal

  • インターフェース制約 (where T : <インターフェース名>)
    Tは指定されたインターフェースを実装している必要がある。
    例: where T : IComparable<T>

  • 構造体 (値型) 制約 (where T : struct)
    Tは値型である必要がある。
    int型、float型、double型、struct型等が該当する。

  • クラス (参照型) 制約 (where T : class)
    Tは参照型である必要がある。
    クラス、インターフェース、デリゲート、配列が該当する。

  • 新しいインスタンス制約 (where T : new())
    Tはパラメータ無しのパブリックコンストラクタを持つ必要がある。

  • 非Null制約 (where T : notnull)
    C# 8.0以降
    Tはnull非許容型である必要がある。

  • アンマネージド制約 (where T : unmanaged)
    Tはアンマネージド型 (ポインタ、列挙型、構造体等) である必要がある。

  • 他の型パラメータとの関係を示す制約
    例: where T : U
    ただし、TはUまたはUのサブクラスである必要がある。


複数の制約の組み合わせ

複数の制約を組み合わせることにより、より具体的な要件を指定できる。

以下の例では、Tは参照型、IComparable<T>を実装、パラメータ無しのコンストラクタを持つ必要がある。

 where T : class, IComparable<T>, new()


制約のメリット

  • 型の安全性
    コンパイル時に型チェックが行われ、不適切な型の使用を防ぐ。
  • パラメータ特性
    型パラメータの特性が明確になるため、ジェネリッククラスやメソッドでその型の特定の機能を使用できるようになる。
  • IntelliSenseのサポート
    制約に基づいて、使用可能なメンバが提案される。
  • パフォーマンスの向上
    コンパイラが型情報を活用して最適化を行うことができる。
  • コードの明確性
    型パラメータの要件が明確になり、コードの意図が理解しやすくなる。


制約の使用例

以下の例では、TはIComparable<T>インターフェースを実装して、かつ、パラメータ無しのコンストラクタを持つ必要がある。

 public class DataProcessor<T>
        where T : IComparable<T>, new()
 {
    public void ProcessData(T data)
    {
       T defaultValue = new T();              // new()制約があるため可能
       if (data.CompareTo(defaultValue) > 0)  // IComparable<T>制約があるため可能
       {
          // 何らかの処理
       }
    }
 }


以下の例では、Tは参照型、IComparable<T>インターフェースを実装、パラメータ無しのコンストラクタを持つ必要があり、
また、Uは値型でなければならない。

 public class GenericClass<T, U> 
        where T : class, IComparable<T>, new()
        where U : struct
 {
    // クラスの実装
 }



拡張メソッドの使用

  1. 呼び出し元において、usingディレクティブを使用して、拡張メソッドの静的クラスを含む名前空間を指定する。
  2. クラスのインスタンスメソッドと同様にメソッドを記述する。
    呼び出し元では、最初の引数は指定しない。
    これは、演算子を適用する型を表すものであり、コンパイラはオブジェクトの型を既に認識しているためである。
    指定する必要があるのは、第2引数以降の引数のみである。


静的クラスに静的メソッドを定義して、その第1引数の前にthisキーワードを付加する時、拡張メソッドになる。
第1引数の型が、拡張される対象となる。

整数型

以下の例では、拡張メソッドAddは、int型を拡張している。(第1引数のthis int)
拡張メソッドに第2引数を指定する時、拡張メソッドの第1引数として渡される。

つまり、拡張メソッドに引数を付加して呼び出す場合、その引数は拡張メソッドの第2引数以降として渡される。

 using System;
 
 namespace SampleNamespace
 {
    public static class SampleExtension
    {
       public static int Add(this int m, int n) => m + n;
    }
 }


 using System;
 using SampleNamespace;  // 拡張メソッドを定義している名前空間
 
 class Program
 {
    static void Main(string[] args)
    {
       int m = 2;
 
       int result = m.Add(3);
 
       // また、通常の静的メソッドとして、SampleExtension.Add(m, 3)のように呼び出すことも可能である
       // SampleExtension.Add(m, 3)
 
       // 定数リテラルでも拡張メソッドを使用することができる
       // int result = 2.Add(3);  出力 : 5
 
       Console.WriteLine($"m.Add(3) : {result}");  // 出力 : 5
    }
 }


浮動小数点型

以下の例では、拡張メソッドAddは、浮動小数点型を拡張している。(第1引数のthis double)
拡張メソッドに第2引数を指定する時、拡張メソッドの第1引数として渡される。

つまり、拡張メソッドに引数を付加して呼び出す場合、その引数は拡張メソッドの第2引数以降として渡される。

 using System;
 
 namespace FloatingPointExtensions
 {
    public static class FloatingPointExtension
    {
       public static double Add(this double m, double n) => m + n;
       public static float Add(this float m, float n) => m + n;
 
       // オーバーロードを追加して、int型の引数も受け付ける場合
       public static double Add(this double m, int n) => m + n;
       public static float Add(this float m, int n) => m + n;
 
       // ジェネリックを使用して型パラメータを導入する場合
       // これにより、様々な数値型に対応する
       public static T Add<T, U>(this T m, U n) where T : struct, IConvertible
                                                where U : struct, IConvertible
       {
          dynamic a = m;
          dynamic b = n;
          return a + b;
       }
    }
 }


ジェネリックを使用して型パラメータを導入する場合、
上記のサンプルコードにおいて、public static T Add<T, U>(this T m, U n)では、2つの型パラメータTとUを使用している。
これにより、メソッドは様々な数値型の組み合わせに対応することができる。

  • 制約
    where <型パラメータ> : <制約 1>, <制約 2>, <制約 3>, ...

    where T : struct, IConvertible
    where U : struct, IConvertible

    これらの制約により、TとUは値型 (struct) であり、かつ、IConvertibleインターフェースを実装している必要がある。
    これは、ほとんどの数値型 (int, float, double等) に当てはまる。

  • dynamicキーワード
    dynamic a = m;
    dynamic b = n;
    dynamicキーワードを使用することにより、コンパイル時ではなく実行時に型チェックが行われる。
    これにより、異なる型同士の加算を可能にしている。

  • 戻り値
    return a + b;
    動的に型が決定された変数aとbを加算して、その結果を返す。
    戻り値の型は、Tになる。


このアプローチのメリットは、1つのメソッドで多くの数値型の組み合わせに対応できることである。
例えば、float型とint型、double型とlong型等の組み合わせでも使用することができる。

ただし、このアプローチにはいくつかの注意点がある。

  • dynamicキーワードの使用により、型安全性が部分的に失われる。
  • パフォーマンスが若干低下する可能性がある。
  • オーバーフローのチェックが行われないため、大きな数値を扱う場合は注意が必要である。


文字列型

以下の例では、CustomExtensions名前空間のStringExtensionクラスを作成して、WordCount拡張メソッドを実装している。
この拡張メソッドは、第1引数で指定したStringクラスを操作する。

 using System.Linq;
 using System.Text;
 using System;
 
 namespace CustomExtensions
 {
    // Extension methods must be defined in a static class.
    public static class StringExtension
    {
       // This is the extension method.
       // The first parameter takes the "this" modifier
       // and specifies the type for which the method is defined.
       public static int WordCount(this String str)
       {
          return str.Split(new char[] {' ', '.','?'}, StringSplitOptions.RemoveEmptyEntries).Length;
       }
    }
 }


 // Import the extension method namespace.
 using CustomExtensions;
 
 namespace Extension_Methods_Simple
 {
    class Program
    {
       static void Main(string[] args)
       {
          string s = "The quick brown fox jumped over the lazy dog.";
          // Call the method as if it were an
          // instance method on the type. Note that the first
          // parameter is not specified by the calling code.
          int i = s.WordCount();
          System.Console.WriteLine("Word count of s is {0}", i);
       }
    }
 }


以下の例では、stringクラスに対してUTF-8エンコードされた文字列を返す拡張メソッドを定義している。

  • エンコード処理
    Encoding.UTF8.GetBytes(value)を使用して、stringクラスをUTF-8バイト配列に変換する。
    Encoding.UTF8.GetString(utf8Bytes)を使用して、そのバイト配列を再びstringクラスに変換する。


  • エラーハンドリング
    if (value == null)で、入力された文字列がnullの場合に例外ArgumentNullExceptionをスローする。
    これにより、nullに対して拡張メソッドを呼び出した場合のエラーを防ぐことができる。


 // 拡張メソッドの定義
 
 using System;
 using System.Text;
 
 namespace <名前空間>
 {
    public static class StringExtensions
    {
       // UTF-8エンコードを行う拡張メソッド
       public static string ToUtf8String(this string value)
       {
          // null チェック
          if (value == null) {
             throw new ArgumentNullException(nameof(value), "Input string cannot be null.");
          }
 
          // UTF-8エンコードを行う
          byte[] utf8Bytes = Encoding.UTF8.GetBytes(value);
 
          // バイト配列から再び文字列に変換(UTF-8でデコード)
          string utf8String = Encoding.UTF8.GetString(utf8Bytes);
 
          return utf8String;
       }
    }
 }
 
 // 拡張メソッドの使用例
 
 using System;
 using <名前空間>;
 
 class Program
 {
    static void Main()
    {
       string originalString = "こんにちは、世界!";
 
       try {
          string utf8EncodedString = originalString.ToUtf8String();
          Console.WriteLine(utf8EncodedString);
       }
       catch (ArgumentNullException ex) {
          Console.WriteLine(ex.Message);
       }
 
       // 文字列がnullの場合
       string nullString = null;
 
       try {
          string result = nullString.ToUtf8String();  // ここで例外がスローされる
       }
       catch (ArgumentNullException ex) {
          Console.WriteLine("Null input detected: " + ex.Message); // ここが呼ばれる
       }
    }
 }


※注意
UTF-8はバイト列のエンコード方式であり、stringをエンコードして再びstringに変換するという操作は、
単に文字列がUTF-8でエンコード可能であることを確認する意味合いに過ぎない場合がある。
本当に必要な処理かどうかを確認することが重要である。


インタフェースおよびジェネリック

インタフェースおよびジェネリックに対しても拡張メソッドも作成することができる。
C#のインタフェースはデフォルトの実装を持つことはできないが、それに近いことが可能である。

 public static bool IsGreaterThan<T>(this IComparable<T> m, T n) where T : struct => (0 < m.CompareTo(n));


 // 使用例
 
 Console.WriteLine($"{2.IsGreaterThan(3)}");       // 出力:False
 Console.WriteLine($"{2.5.IsGreaterThan(2.5)}");   // 出力:False
 Console.WriteLine($"{2.7d.IsGreaterThan(2.5d)}"); // 出力:True
 Console.WriteLine($"{(new DateTime(2017, 11, 15)).IsGreaterThan(new DateTime(2017, 11, 14))}");


ジェネリックメソッド

ジェネリックメソッドとは、型パラメータを使用して定義されるメソッドのことである。
これにより、同じロジックを異なる型に対して適用できる柔軟性の高い設計を行うことができる。

ジェネリックメソッドを使用することにより、型に依存しない汎用的な設計を行うことができ、ソースコードの再利用性と保守性が向上する。

ただし、ジェネリックメソッドが適切でない場合もあるため、使用する状況を慎重に検討する必要がある。

ジェネリックメソッドのメリットを以下に示す。

  • ソースコードの再利用性が向上する。
  • 型安全性を維持しながら、汎用的なソースコードを記述することができる。
  • パフォーマンスが向上する可能性がある。(ボックス化 / アンボックス化の回避)


ジェネリックメソッドの作成手順を以下に示す。

  • メソッド宣言
    通常のメソッド宣言に型パラメータを追加する。
 public static T MethodName<T>(T parameter)


  • 型パラメータの指定
    メソッド名の後にブラケット<>を使用して、その中に型パラメータを指定する。
    <T> または <T, U>


  • 必要に応じて制約を追加
    型パラメータに制約を加えるには、whereキーワードを使用する。
 public static T MethodName<T>(T parameter) where T : struct


  • メソッド本体の実装
    型パラメータを使用してメソッドのロジックを実装する。


※注意
型パラメータは慣習的に大文字のTから始まる。(T, U, V, ....)
複雑な制約を設定する場合は、可読性に注意すること。

以下の例では、2つの値を交換するジェネリックメソッドを作成している。

 public static void Swap<T>(ref T a, ref T b)
 {
    T temp = a;
    a = b;
    b = temp;
 }
 
 // 使用例
 int x = 5,
     y = 10;
 
 Swap(ref x, ref y);
 Console.WriteLine($"x: {x}, y: {y}");  // 出力: x: 10, y: 5
 
 string s1 = "Hello",
        s2 = "World";
 Swap(ref s1, ref s2);
 Console.WriteLine($"s1: {s1}, s2: {s2}");  // 出力: s1: World, s2: Hello


メソッドチェーン

メソッドチェーンとは、メソッドを鎖のように続けて記述することである。
例えば、LINQでは <プロパティ名>.Where(/* 処理 1 */).Select(/* 処理 2 */) のようにメソッドを繋げて記述することができる。

以下の例では、拡張メソッドAddと拡張メソッドMultiplyを定義する場合、メソッドチェーンが使用できる。

 // 拡張メソッド
 
 public static int Add(this int m, int n) => m + n;
 
 public static int Multiply(this int m, int n) => m * n;


 // 使用例
 
 int result = 2.Add(3).Multiply(4);
 Console.WriteLine($"2.Add(3).Multiply(4)→{result}");  // 出力 : 20