かずきのBlog@hatena

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

Managed Extensibility Framework入門 その3「Export」

さて、前回はMEFを使ってHello world的なプログラムを作成しました。基本的には、クラスにExport属性をつけてカタログに公開するという設定をして、Import属性をつけてカタログ内のクラスを設定してもらうという流れでした。ということで、ExportとImportの指定の仕方で、好きなようにクラスを組み上げることが出来るということです。なので、今回はまずExportを、少し掘り下げてみてみようと思います。

前回のおさらい

前回、以下のようなHello MEF!!と表示されるプログラムを作成しました。

namespace HelloMEF
{
    using System;
    using System.ComponentModel.Composition;
    using System.ComponentModel.Composition.Hosting;
    using System.Reflection;

    class Program
    {
        static void Main(string[] args)
        {
            // 現在実行中のアセンブリからカタログを作成する
            var catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());

            // 現在実行中のアセンブリのカタログを元にコンテナを作る
            var container = new CompositionContainer(catalog);

            // Aクラスのインスタンスを取得する
            var typeA = container.GetExportedValue<A>();

            // メッセージ出力!
            typeA.Foo();
        }
    }

    // Export属性を使ってカタログに収集してもらう対象にする
    [Export]
    public class A
    {
        // このプロパティの型に一致するクラスのインスタンスを
        // コンテナ内から探してセットしてという意味
        [Import]
        public B Value { get; set; }

        // Bクラスを使って処理をする
        public void Foo()
        {
            this.Value.Bar();
        }
    }

    [Export]
    public class B
    {
        public void Bar()
        {
            // 念願のメッセージを表示します
            Console.WriteLine("Hello MEF!!");
        }
    }
}

こいつのMainを少し変えてExportで取得できるインスタンスの特徴を見てみようと思います。まず、Mainメソッドで2回ほどAクラスのインスタンスを取得して取得できた値を比較します。

// 現在実行中のアセンブリからカタログを作成する
var catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());

// 現在実行中のアセンブリのカタログを元にコンテナを作る
var container = new CompositionContainer(catalog);

// Aクラスのインスタンスを2つ取得する
var typeA1 = container.GetExportedValue<A>();
var typeA2 = container.GetExportedValue<A>();

// typeA1とtypeA2を比較してみると・・・
Console.WriteLine("typeA1 == typeA2 -> {0}", typeA1 == typeA2);

実行すると、以下のように表示されます。

typeA1 == typeA2 -> True

この結果から以下のことがわかります。

  • Exportしたクラスを何回取得しても同じインスタンスが返ってくる。

デフォルトの挙動は、こうなります。覚えておきましょう!

2つ以上インスタンスが欲しいんだけど?

つまり、デフォルトだとコンテナ内でのインスタンスは1つということになります。ある意味シングルトンパターンみたいなことをしなくてもMEFのコンテナ内で勝手にシングルトンとして管理してくれるので有りがたい面もあります。しかし、全てがシングルトンだと困るということもあります。そういう時にどうするのか?というのを見てみましょう。

作成時のポリシーを設定する

Export属性と同時に使用できる属性としてPartCreationPolicy

  • System.ComponentModel.Composition.PartCreationPolicyAttribute
    • Exportされたクラスのインスタンスを生成するときの動作を設定する

PartCreationPolicyにはCreationPolicy列挙体の値を設定します。CreationPolicyの値は以下のようなものがあります。

  • CreationPolicy構造体
    • Shared : 全体で共有する。つまりシングルトン。
    • NonShared : 共有しない。つまり毎回インスタンスを作る。
    • Any : Sharedを求められるならSharedでNonSharedを求められるならNonSharedにもなる柔軟な感じ

では、AクラスにPartCreationPolicyを設定してみます。今回は、取得のたびにインスタンスを変えてみたいのでCreationPolicyはNonSharedになります。

// Export属性を使ってカタログに収集してもらう対象にする
[Export]
// インスタンスを共有しないようにする
[PartCreationPolicy(CreationPolicy.NonShared)]
public class A
{
    // このプロパティの型に一致するクラスのインスタンスを
    // コンテナ内から探してセットしてという意味
    [Import]
    public B Value { get; set; }

    // Bクラスを使って処理をする
    public void Foo()
    {
        this.Value.Bar();
    }
}

そして、Mainメソッドを以下のように書き換えます。Aクラスのインスタンス同士の比較とAクラスが参照しているBクラスのインスタンス同士を比較しています。

// 現在実行中のアセンブリからカタログを作成する
var catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());

// 現在実行中のアセンブリのカタログを元にコンテナを作る
var container = new CompositionContainer(catalog);

// Aクラスのインスタンスを2つ取得する
var typeA1 = container.GetExportedValue<A>();
var typeA2 = container.GetExportedValue<A>();

// typeA1とtypeA2を比較してみると・・・
Console.WriteLine("typeA1 == typeA2 -> {0}", typeA1 == typeA2);
// ついでにAクラスが参照しているBクラスが同一なのか確認
Console.WriteLine("typeA1.Value == typeA2.Value -> {0}", typeA1.Value == typeA2.Value);

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

typeA1 == typeA2 -> False
typeA1.Value == typeA2.Value -> True

Aクラスのインスタンスは共有しないように設定したので、違うインスタンスが返されてますが、Bクラスは設定を変更していないため同一のインスタンスが返ってきます。

このようにPartCreationPolicy属性を付けることで、同一インスタンスを使いまわすのか、コンテナからインスタンスを取得する度に違うインスタンスを取得するのか選択できるようになります。

Exportに名前をつけよう

ここでは、小技ですがExport時に任意の名前でExportするようにしてみます。やり方は簡単で、Export属性のコンストラクタに名前を渡すだけです。AクラスについてるPartCreationPolicyはいったん外して、Export属性に名前を渡すようにします。

// 田中という名前でExport
[Export("田中")]
public class A
{
    // このプロパティの型に一致するクラスのインスタンスを
    // コンテナ内から探してセットしてという意味
    [Import]
    public B Value { get; set; }

    // Bクラスを使って処理をする
    public void Foo()
    {
        this.Value.Bar();
    }
}

そして、Mainの中を以下のように書き換えます。

// 現在実行中のアセンブリからカタログを作成する
var catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());

// 現在実行中のアセンブリのカタログを元にコンテナを作る
var container = new CompositionContainer(catalog);

// Aクラスで田中という名前で登録されてるものを取得
var a = container.GetExportedValue<A>("田中");

// 処理実行
a.Foo();

実行すると、以下のように表示されます。ちゃんと動いてますね。

Hello MEF!!

試しに、Mainメソッド内でGetExportedValueで名前を指定している箇所を別の名前にして試してみます。

var a = container.GetExportedValue<A>("しばやん");

実行すると、以下のように例外で怒られます。

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

// 以下スタックトレースが続く

名前をつけてExportしたものは、責任を持って名前を指定して取得しないといけません。
今回は以上です。次はImportあたりの説明が出来たらいいなと思ってます。