C#

【C#】ジェネリックデリゲートと定義済みデリゲート

この記事ではジェネリックデリゲートと定義済みデリゲートについて簡単なコードで解説しています。定義済みデリゲートではAction,Func,Predicate,Comparison,Converterデリゲートを扱います。

なお【C#】デリゲートの基本 や【C#】匿名メソッドとラムダ式 の知識を前提として説明しますので、併せてご覧ください。

ジェネリックデリゲート

ジェネリックはパラメータ化された型のことで、ジェネリッククラスがよく使用されますが、デリゲートの場合もジェネリックデリゲートにすることができます。ジェネリックデリゲートは以下のように宣言します。

delegate 戻り値の型 デリゲート名<型パラメータリスト>(仮引数リスト)

デリゲート名の後に<型パラメータリスト>があるのがポイントです。簡単な例を見てみます。

コード

using System;

internal class Program
{
    //ジェネリックデリゲートの宣言
    delegate double InvDel<T>(T x);

    static void Main(string[] args)
    {
        InvDel<int> InvIntDel = new InvDel<int>(InverseInt); //インスタンス化
        InvDel<double> InvDblDel = new InvDel<double>(InverseDbl); //インスタンス化

        double ans;

        ans = InvIntDel(5);
        Console.WriteLine("ans:{0}", ans);

        ans = InvDblDel(0.1);
        Console.WriteLine("ans:{0}", ans);
    }

    static double InverseInt(int a)
    {
        Console.WriteLine("InverseInt({0})が呼び出されました", a);
        return 1 / (double)a;
    }

    static double InverseDbl(double a)
    {
        Console.WriteLine("InverseDbl({0})が呼び出されました", a);
        return 1 / a;
    }
}

出力結果

InverseInt(5)が呼び出されました
ans:0.2
InverseDbl(0.1)が呼び出されました
ans:10

上記コードでは、6行目で型パラメータTを用いて、ジェネリックデリゲートの宣言をしています。戻り値はdouble型でT型の引数を一つ持つInvDel<T>ジェネリックデリゲートを宣言です。型パラメータにint型を当てはめたインスタンスを10行目、型パラメータにdouble型を当てはめたインスタンスを11行目で生成しています。

次に下記の例のようにデリゲートの戻り値も型パラメータ化することができます。

コード

using System;

internal class Program
{
    //ジェネリックデリゲートの宣言
    delegate T AddDel<T>(T x, T y);

    static void Main(string[] args)
    {
        AddDel<string> AddStrDel = new AddDel<string>(AddStr); //インスタンス化
        AddDel<int> AddIntDel = new AddDel<int>(AddInt); //インスタンス化

        string ansStr = AddStrDel("abc", "def");
        Console.WriteLine("ansStr:{0}", ansStr);

        int ansInt = AddIntDel(2, 3);
        Console.WriteLine("ansInt:{0}", ansInt);
    }

    static string AddStr(string a, string b)
    {
        Console.WriteLine("AddStr({0},{1})が呼び出されました", a, b);
        return a + b;
    }

    static int AddInt(int a, int b)
    {
        Console.WriteLine("AddInt({0},{1})が呼び出されました", a, b);
        return a + b;
    }
}

出力結果

AddStr(abc,def)が呼び出されました
ansStr:abcdef
AddInt(2,3)が呼び出されました
ansInt:5

上記の例では、6行目で型パラメータTを用いて、戻り値と引数2つがT型のジェネリックデリゲートAddDelを宣言しています。10行目で型パラメータTにstring型を当てはめたAddDel<string>型をインスタンス化しており、11行目では型パラメータTにint型を当てはめたAddDel<int>型をインスタンス化しています。

型パラメータリストですので、複数の型パラメータを持つデリゲートも利用可能です。次の例を見てみましょう。

コード

using System;

internal class Program
{
    //ジェネリックデリゲートの宣言
    delegate T1 AddDel<T1, T2>(T1 x, T2 y);

    static void Main(string[] args)
    {
        AddDel<int, string> AddIntDel = new AddDel<int, string>(AddInt);
        AddDel<string, int> AddStrDel = new AddDel<string, int>(AddStr);

        int ans1 = AddIntDel(123, "456");
        Console.WriteLine("ans1:{0}", ans1);

        string ans2 = AddStrDel("123", 456);
        Console.WriteLine("ans2:{0}", ans2);
    }

    static int AddInt(int a, string b)
    {
        Console.WriteLine("AddInt({0},{1})が呼び出されました", a, b);
        return a + int.Parse(b);
    }

    static string AddStr(string a, int b)
    {
        Console.WriteLine("AddStr({0},{1})が呼び出されました", a, b);
        return a + b.ToString();
    }
}

出力結果

AddInt(123,456)が呼び出されました
ans1:579
AddStr(123,456)が呼び出されました
ans2:123456

上記コードの例では型パラメータをT1,T2の2つを持つデリゲートAddDelを宣言しています。このデリゲートの戻り値はT1型です。10行目で型パラメータT1をint型、T2をstring型にしたAddDel<int,string>型の変数AddIntDelをインスタンス化しています。このAddDel<int,string>型の戻り値の型はint型です。11行目では、T1をstring型、T2をint型にしたAddDel<string,int>型の変数AddStrDelをインスタンス化しています。このAddDel<string,int>型の戻り値はstring型です。

以上でジェネリックデリゲートの基本は終わりですが、基本的にジェネリッククラスやジェネリックメソッドと同じなので、特に難しいことはありません。

定義済みデリゲート

デリゲートにはあらかじめ定義された定義済みデリゲートがあります。定義済みデリゲートは.NETに含まれる、宣言しなくても利用できるデリゲートです。

コード

using System;

internal class Program
{
    //ジェネリックデリゲートの宣言
    //delegate void Action<T>(T x);

    static void Main(string[] args)
    {
        Action<int> actDel = delegate (int val) { Console.WriteLine("val={0}です", val); };

        actDel(5);
    }
}

出力結果

val=5です

上記コードでは10行目でAction<int>型のデリゲート変数actDelをインスタンス化しています。しかし、6行目を見ると、Action<T>デリゲートの宣言がコメントアウトされています。本来6行目の宣言が必要に思われますが、Action<T>デリゲートはSystem名前空間に含まれているため、宣言は不要なのです。※仮にコメントアウトをはずしても上記コードの例では同じ動作をします。

このようなデリゲートは定義済み(built-in)デリゲートと呼ばれます。主なものに、Action、Func、Predicate、Comparisonなどがあります。

型名戻り値の型型パラメータ備考
ActionvoidT1~T16戻り値がないときに使用
Func型パラメータ(TResult)で指定T1~T16,TResult戻り値があるときに使用
PredicateboolT条件が満たされているかどうか
ComparisonintT2変数の比較
ConverterToutputTInput, TOutput特定の型のオブジェクトを別の型のオブジェクトに変換する

これらについて、わかりやすいコードで解説していきます。

Actionデリゲート

Actionデリゲートはメソッドに戻り値がない場合に使用する定義済みデリゲートです。型パラメータが不要なとき(=引数がないとき)にはActionを使用して宣言し、型パラメータが必要なときには、Action<型パラメータリスト>を使用して宣言します。型パラメータリストは下記のように最大16まで記述可能です。

Action
Action<T>
Action<T1, T2>

Action<T1, T2, …, T16>

このとき型パラメータリストの数=仮引数の数になります。自分で定義するカスタムデリゲートでは型パラメータの数は仮引数の数と異なっている場合もありますが、Actionデリゲートでは型パラメータの順に仮引数が対応します。すなわち、Actionデリゲートでは、型パラメータリストの型順と数は、登録されるメソッドの型順と数と一致している必要があります。

以下にActionデリゲートの使用例を示します。

コード

using System;

internal class Program
{
    static void Main(string[] args)
    {
        Action msgDel = () => Console.WriteLine("msgDel()が呼び出されました.");
        msgDel();

        Action<int> printValDel = x => Console.WriteLine("printValDel({0})が呼び出されました.", x);
        printValDel(10);

        Action<int, double> divDel = (x, y) => Console.WriteLine("divDel({0},{1})が呼び出されました.割り算の結果:{2}です.", x, y, x / y);
        divDel(3, 1.5);
    }
}

出力結果

msgDel()が呼び出されました.
printValDel(10)が呼び出されました.
divDel(3,1.5)が呼び出されました.割り算の結果:2です.

上記コードでは、7行目で型パラメータがない場合、10行目で型パラメータが1個の場合、13行目で型パラメータが2個の場合のActionデリゲートをインスタンス化しています。13行目では型パラメータはint型, double型の順番で2つなので、登録されるメソッドの仮引数の数もint型,double型の順番で2つになります。

Funcデリゲート

Funcデリゲートはメソッドに戻り値がある場合に使用する定義済みデリゲートです。Funcデリゲートは、Func<型パラメータリスト>型パラメータリストを使用して宣言します。型パラメータリストには下記のように1~17個の型を指定します。最後に指定した型(下記のTResult)が戻り値の型になります。つまり最低でも戻り値の型は指定しなければなりません。

Func<TResult>
Func<T, TResult>
Func<T1, T2, TResult>

Func<T1, T2, …, T16, TResult>

このときTResultを除く型パラメータリストの数=仮引数の数になります。FuncデリゲートではTResultを除く型パラメータの順に仮引数が対応します。すなわち、Actionデリゲートでは、Tresultを除く型パラメータリストの型順と数は、登録されるメソッドの型順と数と一致している必要があります。

以下にFuncデリゲートの使用例を示します。

コード

using System;

internal class Program
{
    static void Main(string[] args)
    {
        Func<double> GetminDel = () => System.Environment.TickCount / 1000 / 60;
        Console.WriteLine("システムが開始されてから{0:N3}minです", GetminDel());

        Func<int, double> GetInvDel = x => 1 / (double)x;
        int val1 = 5;
        Console.WriteLine("{0}の逆数は{1}です", val1, GetInvDel(val1));

        Func<int, double, string> GetDivDel = (x, y) => String.Format("{0}割る{1}は約{2:N1}です", x, y, (double)x / y);
        int val2 = 5;
        double divider = 0.12;
        Console.WriteLine(GetDivDel(val2, divider));
    }
}

出力結果

システムが開始されてから49.000minです
5の逆数は0.2です
5割る0.12は約41.7です

上記コードでは、7行目で型パラメータがひとつ(=引数がなく戻り値だけ)の場合、10行目で型パラメータが2個(引数1つと戻り値)の場合、13行目で型パラメータが3個(引数2つと戻り値)の場合のFuncデリゲートをインスタンス化しています。13行目では型パラメータはint型, double型, string型の順番で3つなので、登録されるメソッドの仮引数の数もint型,double型の順番で2つとなり戻り値がstring型となります。

FuncデリゲートとActionデリゲートの違いは戻り値の有無だけなので、まとめて覚えておきましょう。

Predicateデリゲート

Predicateデリゲートは、指定されたオブジェクトが条件を満たすかどうかを判断するメソッドを表します。戻り値はbool型でひとつの型パラメータTとT型の引数をひとつ持ちます。つまり、Predicate<T>はFunc(T, bool)と同じです。Predicateデリゲートは以下のように定義されています。

public delegate bool Predicate<in T>(T obj);

Func<T, bool>の代わりに使用することもできますが、通常は検索条件を指定したりする場合に使います。例えば、List<T>のRemoveAllメソッドの引数はPredicate<T>デリゲートです。具体的な例を見てみましょう。

コード

using System;
using System.Collections.Generic;

internal class Program
{
    static void Main(string[] args)
    {
        var lst = new List<string> { "Japan", "Vietnam", "India", "USA", "Canada", "Mexico" };
        Console.WriteLine("{0}:[{1}]", nameof(lst), string.Join(" ", lst));  //リストの表示

        Predicate<string> pdDel = item => item.Contains("a");
        lst.RemoveAll(pdDel);
        Console.WriteLine("{0}:[{1}]", nameof(lst), string.Join(" ", lst));  //リストの表示
    }
}

出力結果

lst:[Japan Vietnam India USA Canada Mexico]
lst:[USA Mexico]

上記コードでは、11行目でPredicate<string>デリゲートがインスタンス化されています。インスタンス化されたpdDelに登録されたメソッドの処理内容は、「string型の仮引数itemに対して、”a”という文字列が含まれていたらTrue、含まれていなければFalseを返す」というものです。12行目で、このpdDelがListのRemoveAllメソッドの引数となっています。RemoveAllメソッドは、引数となっているpdDelの処理に基づいてbool判定し、Trueであれば削除してFalseであれば削除しないで残します。したがって、12行目の処理は、「Listから”a”の含まれたものを削除する」ということになります。

このようにPredicate<T>は、bool判定する条件などを指定するときに、メソッドの引数として渡されることがあります。※メソッドの引数としてPredicate<T>が指定されている場合にはFunc<T, bool>を代用することはできません。

なお上記コードは、以下のようにPredicate<T>型の変数の宣言をしなくても同じ処理ができます。下記コード10行目は上記コード11~12行目をまとめて記述しています。むしろ下記のようにPredicate<T>型の引数の箇所に直接ラムダ式で書くのが一般的です。

コード

using System;
using System.Collections.Generic;

internal class Program
{
    static void Main(string[] args)
    {
        var lst = new List<string> { "Japan", "Vietnam", "India", "USA", "Canada", "Mexico" };
        Console.WriteLine("{0}:[{1}]", nameof(lst), string.Join(" ", lst));  //リストの表示

        lst.RemoveAll(item => item.Contains("a"));
        Console.WriteLine("{0}:[{1}]", nameof(lst), string.Join(" ", lst));  //リストの表示
    }
}

Comparisonデリゲート

Comparisonデリゲートは、同じ型の2つのオブジェクトを比較するメソッドを表します。ひとつの型パラメータTを持ち、T型の引数を2つ持ちます。戻り値の型はint型です。

public delegate int Comparison<in T>(T x, T y);

Comparisonデリゲートは、2つの変数x,yの大小を比較し、その結果をint型で返すメソッドを表しています。例えばListのSortメソッドの引数として使用されています。2つの引数x,yの大小関係と戻り値の関係は以下のようになります。

引数の大小関係戻り値
x<y戻り値<0
x=y戻り値=0
x>y戻り値>0

実際にListのSortメソッドでの例を見てみましょう。

コード

using System;
using System.Collections.Generic;

internal class Program
{
    static void Main(string[] args)
    {
        var lst = new List<string> { "cat", "rabbit", "camel", "elephant", "fish" };
        Console.WriteLine("{0}:[{1}]", nameof(lst), string.Join(" ", lst));  //リストの表示

        Comparison<string> cmpDel = (x, y) => x.Length - y.Length;
        lst.Sort(cmpDel);
        Console.WriteLine("{0}:[{1}]", nameof(lst), string.Join(" ", lst));  //リストの表示
    }
}

出力結果

lst:[cat rabbit camel elephant fish]
lst:[cat fish camel rabbit elephant]

上記コードの例では11行目でComparison<string>デリゲートがインスタンス化されています。インスタンス化されたcmpDelに登録されたメソッドの処理内容は、「string型の仮引数xの長さ引くyの長さを返す」というものです。12行目で、このcmpDelがListのSortメソッドの引数となっています。 このようにすることで文字列xが文字列yより長ければ正の値となり、Sortメソッドはyのほうがxより小さいオブジェクトと判断してソートします。Sortメソッドを引数のないデフォルト条件で使用すると、abc順(辞書の順)にソートしてしまいますが、引数にComparisonデリゲートを指定することで、カスタム化したソートができるようになります。

なお、上記コードの11~12行目は下記のように一行にまとめることもできます。

lst.Sort((x, y) => x.Length – y.Length);

このようにまとめればComparisonデリゲートを宣言することなく利用することができます。

Converterデリゲート

Converterデリゲートは、特定の型のオブジェクトを別の型のオブジェクトに変換するメソッドを表します。TInputとTOutputの2つの型パラメータを持ちます。TInput型の引数をひとつ持ち、TOutput型の戻り値を持ちます。Converter<TInput, Toutput>は以下のように定義されています。

public delegate TOutput Converter<in TInput,out TOutput>(TInput input);

TOutputには戻り値の型を指定しTInputには引数の型を指定します。つまりConverterデリゲートはTInput型の変数をTOutput型の変数に変換するメソッドを表します。例えばListを型変換するためのConvertAllメソッドの引数としてConverterデリゲート変数が渡されます。例を見てみましょう。

コード

using System;
using System.Collections.Generic;

internal class Program
{
    static void Main(string[] args)
    {
        var lstChar = new List<char> { 'a', 'b', 'c', 'd' };
        Console.WriteLine("{0}:[{1}]", nameof(lstChar), string.Join(" ", lstChar));  //リストの表示

        Converter<char, int> intConvDel = new Converter<char, int>(val => (int)val);
        List<int> lstInt = lstChar.ConvertAll(intConvDel);
        Console.WriteLine("{0}:[{1}]", nameof(lstInt), string.Join(" ", lstInt));  //リストの表示
    }
}

出力結果

lstChar:[a b c d]
lstInt:[97 98 99 100]

上記コードでは11行目でConverterデリゲート型の変数intConvDelを宣言し、ラムダ式を用いてint型への変換のメソッドでインスタンス化しています。次の12行目では、ListオブジェクトのConvertAllメソッドの引数としてintConvDelを渡しています。このようにconverterデリゲートでは、型変換の方法を指定するために使用されます。なお、この11~12行目は下記のように一行にまとめることもできます。

List lstInt = lstChar.ConvertAll(item => (int)item);

このようにまとめればConverterデリゲートを宣言することなく利用することができます。

終わりに

ここでは、ジェネリックデリゲートと定義済みデリゲートについて扱いました。ジェネリックデリゲートや定義済みデリゲートがわかると、メソッドの引数にラムダ式を入れるわけもわかります。

ABOUT ME
しかすけ
日々是好日。何とか何とか生きてます。

COMMENT

メールアドレスが公開されることはありません。 が付いている欄は必須項目です