かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

Managed Extensibility Framework入門 その4「もっとExport」

さて、前回ではExportについてちょっと深く見てきました。前回の最後で次はImportかなと言っておいて申し訳ないのですが、もうちょっとExportにお付き合い頂きたいと思います。

Export時の型を指定

今までExportを付けた型は、AクラスにExportをつけたらAクラスの型としてExportしていました。実はこれ、型をA以外としてExportすることも出来ます。正確に言うと、ベースクラスや実装しているインターフェースの型としてExportすることが出来ます。どういうことか見てみましょう。

まずは、型を指定しない今までのケースでプログラムを書いてみます。

// 挨拶屋
interface IGreeter
{
    void Greet();
}

// まずは普通にExport
[Export]
class Tanaka : IGreeter
{
    public void Greet()
    {
        Console.WriteLine("こんにちは!たなかです!");
    }
}

上記のように挨拶をするだけの単純なインターフェースを定義して、それを実装しているTanakaクラスを定義しています。そして、TnakaクラスにExport属性を指定しています。では、これを使うコードをMainに書いていきます。

static void Main(string[] args)
{
    // コンテナを作るところはお馴染み
    var catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());
    var container = new CompositionContainer(catalog);

    // Tanakaさんをくださいな
    var g = container.GetExportedValue<Tanaka>();
    // 挨拶実行
    g.Greet();
}

実行結果は予想どおりだと思いますが以下のようになります。

こんにちは!たなかです!

では、TanakaクラスとしてではなくてIGreeterインターフェースとしてコンテナから取得したい場合はどうなるでしょう?とりあえず安直にGetExportedValueの部分をIGreeterに変更して実行してみます。

static void Main(string[] args)
{
    // コンテナを作るところはお馴染み
    var catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());
    var container = new CompositionContainer(catalog);

    // Tanakaさんをくださいな
    var g = container.GetExportedValue<IGreeter>();
    // 挨拶実行
    g.Greet();
}

実行すると、例外が発生します。

ハンドルされていない例外: System.ComponentModel.Composition.ImportCardinalityMis
matchException: 制約 '((exportDefinition.ContractName == "HelloMEF.IGreeter") An
dAlso (exportDefinition.Metadata.ContainsKey("ExportTypeIdentity") AndAlso "Hell
oMEF.IGreeter".Equals(exportDefinition.Metadata.get_Item("ExportTypeIdentity")))
)' に一致する有効なエクスポートが見つかりませんでした。無効なエクスポートが拒否
されている可能性があります。

色々書いてありますがIGreeterでカタログに登録されているものはないということですね。確かにTnakaクラスにはExportをつけましたが、IGreeterにはExportをつけていません。しかし、インターフェースを直接Exportしたとしてもインスタンス化できないものなのでどうにもなりません。そんな時のために、Export属性でExportするときの型を指定する機能を使います。使い方は簡単でExportの引数にTypeを渡すだけです。
では、先ほどのTanakaクラスをExportしている箇所を以下のように書き換えてみます。

// 挨拶屋
interface IGreeter
{
    void Greet();
}

// IGreeterとしてTnakaクラスをExport
[Export(typeof(IGreeter))]
class Tanaka : IGreeter
{
    public void Greet()
    {
        Console.WriteLine("こんにちは!たなかです!");
    }
}

この状態でプログラムを実行すると、以下のような結果になります。

こんにちは!たなかです!

ばっちりですね。

同じ型で複数個Export

インターフェースを実装した型をインターフェースの型としてExportする方法を紹介しました。では、インターフェースを実装するクラスが複数あって、それぞれがインタフェースの型としてExportしたらどうなるか見てみようと思います。

もう1つOkazukiというクラスを定義して、このクラスもIGreeterとしてExportします。

// 挨拶屋
interface IGreeter
{
    void Greet();
}

// IGreeterとしてTnakaクラスをExport
[Export(typeof(IGreeter))]
class Tanaka : IGreeter
{
    public void Greet()
    {
        Console.WriteLine("こんにちは!たなかです!");
    }
}

// IGreeterとしてOkazukiクラスをExport
[Export(typeof(IGreeter))]
class Okazuki : IGreeter
{
    public void Greet()
    {
        Console.WriteLine("犬のアイコンです");
    }
}

この状態でプログラムを実行すると、以下のような結果になります。

ハンドルされていない例外: System.ComponentModel.Composition.ImportCardinalityMis
matchException: 制約 '((exportDefinition.ContractName == "HelloMEF.IGreeter") An
dAlso (exportDefinition.Metadata.ContainsKey("ExportTypeIdentity") AndAlso "Hell
oMEF.IGreeter".Equals(exportDefinition.Metadata.get_Item("ExportTypeIdentity")))
)' に一致するエクスポートが複数見つかりました。

複数見つかったと言われているので、2つ見つかったけどどっちにしていいのかわからない状態になっています。これを解決するにはいくつか方法があります。まずは、前回やった名前をつけてやる方法です。以下のようにTanakaクラスとOkazukiクラスのExportに名前を指定してやります。

// IGreeterとしてTnakaクラスをExport
[Export("Tanaka", typeof(IGreeter))]
class Tanaka : IGreeter
{
    public void Greet()
    {
        Console.WriteLine("こんにちは!たなかです!");
    }
}

// IGreeterとしてOkazukiクラスをExport
[Export("Okazuki", typeof(IGreeter))]
class Okazuki : IGreeter
{
    public void Greet()
    {
        Console.WriteLine("犬のアイコンです");
    }
}

このクラスを取得する側では、型と名前を指定して一意に取得するクラスを決定できるようにします。

static void Main(string[] args)
{
    // コンテナを作るところはお馴染み
    var catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());
    var container = new CompositionContainer(catalog);

    // Tanakaさんをくださいな
    var t = container.GetExportedValue<IGreeter>("Tanaka");
    // 挨拶実行
    t.Greet();

    // Okazukiさんをくださいな
    var o = container.GetExportedValue<IGreeter>("Okazuki");
    // 挨拶実行
    o.Greet();
}

これを実行すると、以下のようになります。ちゃんとTanakaクラスとOkazukiクラスの処理が実行されているのがわかります。

こんにちは!たなかです!
犬のアイコンです


次の解決方法は、IGreeterとしてExportされている型を全部とってくるというよくばりな方法です。先ほどTanakaクラスとOkazukiクラスに追加した名前を消して、IGreeterとしてExportされている型が2つある状態にします。

// IGreeterとしてTnakaクラスをExport
[Export(typeof(IGreeter))]
class Tanaka : IGreeter
{
    public void Greet()
    {
        Console.WriteLine("こんにちは!たなかです!");
    }
}

// IGreeterとしてOkazukiクラスをExport
[Export(typeof(IGreeter))]
class Okazuki : IGreeter
{
    public void Greet()
    {
        Console.WriteLine("犬のアイコンです");
    }
}

そして、Mainを以下のように変更します。CompositionContainerクラスのGetExportedValues(複数形なのがポイント)を使うことで、指定した型のインスタンスを全て取得できるようになっています。戻り値はIEnumerableになります。

static void Main(string[] args)
{
    // コンテナを作るところはお馴染み
    var catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());
    var container = new CompositionContainer(catalog);

    // IGreeterを全部くれ頼む
    IEnumerable<IGreeter> greeters = container.GetExportedValues<IGreeter>();
    // 全員に挨拶させる
    foreach (var g in greeters)
    {
        g.Greet();
    }
}

実行すると以下のようになります。

こんにちは!たなかです!
犬のアイコンです

ちゃんと実行されました。

まとめ

ということで今回のまとめです。

  • Exportするときに型を指定できる
    • 指定できる型はベースクラスか実装しているインターフェースの型
  • 複数のクラスを同じ型としてExportすることが出来る
    • 1つのインスタンスを指定して取得する必要があるときはExport時に名前をつけて型と名前で識別できるようにする
    • GetExportedValuesを使って指定した型でExportされているインスタンスを全て取得する方法もある

以上です!次こそはImport!!