かずきのBlog@hatena

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

Managed Extensibility Framework入門 その7「クラス以外のExportとImport」

さて、前回までで一番基本的なクラスのExportとImportについて説明しました。そして、それとDirectoryCatalogを組み合わせて、拡張可能なアプリケーションを実際に作ってみるところまでやりました。(拡張可能アプリケーションといってもしょぼいですが・・・)


ここでは、個人的にMEF以外のDIコンテナではあまり見ないクラス以外のExportとImportについて紹介したいと思います。

プロパティのExport

今までクラス宣言の部分にExportをつけていましたが、実はこのExportは、プロパティやメソッドについてもつけることができます。ここでは、プロパティについてExportを付けると、どうなるかということを説明します。


プロパティにExportを付けると、プロパティを取得した結果をExportすることが出来ます。実際に簡単な例で見てみましょう。

public class Greeter
{
    // string型で挨拶という名前でプロパティをExport
    [Export("挨拶")]
    public string Greet
    {
        get
        {
            return "こんにちは";
        }
    }
}

GreeterというクラスのGreetプロパティにExport属性をつけています。コメントにもあるように、こうすることでプロパティの型(ここではstring)でExportすることが出来ます。クラスの時と同じように名前を指定してExportすることもできます。例では挨拶という名前でExportしています。もちろん名前無しでExportも可能です。


では、実際に値が取れるかやってみようと思います。Mainメソッドを以下のようにして値を取得してみて結果を確認してみました。

static void Main(string[] args)
{
    // 手慣れたコンテナの作成
    var container = new CompositionContainer(
        new AssemblyCatalog(Assembly.GetExecutingAssembly()));

    // string型で挨拶という名前でExportされているものを取得
    var greetingMessage = container.GetExportedValue<string>("挨拶");
    // 表示して内容を確認
    Console.WriteLine(greetingMessage);
}

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

こんにちは

きちんと、プロパティをExportした結果がとれていることがわかります。因みに、このプロパティの値を複数回コンテナから取得したときってどうなるんだろう?という疑問がわいてきました。

考えられる可能性としては以下の3パターンくらいだと思います。

  • 毎回Greeterのインスタンスを作成してGreetプロパティの値を取得する
  • GreeterのインスタンスはキャッシュしておいてGreetプロパティから値を取得する
  • Greetプロパティから取得した値はキャッシュしておいて、その値を返す。

この違いがわかるように、プログラムの要所要所にログを埋め込んで、2回コンテナから値を取得して結果を表示してみました。

using System;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
using System.Reflection;

namespace MEFExportAdv
{
    class Program
    {
        static void Main(string[] args)
        {
            // 手慣れたコンテナの作成
            var container = new CompositionContainer(
                new AssemblyCatalog(Assembly.GetExecutingAssembly()));

            // 2回取得して表示
            {
                var greetingMessage = container.GetExportedValue<string>("挨拶");
                Console.WriteLine(greetingMessage);
            }
            {
                var greetingMessage = container.GetExportedValue<string>("挨拶");
                Console.WriteLine(greetingMessage);
            }
        }
    }

    public class Greeter
    {
        public Greeter()
        {
            Console.WriteLine("Greeterのインスタンスが作られました");
        }

        // string型で挨拶という名前でプロパティをExport
        [Export("挨拶")]
        public string Greet
        {
            get
            {
                Console.WriteLine("プロパティから値が取得されました");
                return "こんにちは";
            }
        }
    }
}

GreeterのコンストラクタとGreetプロパティのgetにログを埋め込んでます。そしてMainで2回値を取得して表示しています。では、実行してみましょう。

Greeterのインスタンスが作られました
プロパティから値が取得されました
こんにちは
こんにちは

ということで結果は、プロパティの値はキャッシュされて同じ値が返され続けるみたいです。

でも、よく考えてみるとクラスのExportの時もPartCreationPolicyを使うことで、コンテナからインスタンスを取得する際のインスタンスを再利用するか、毎回作成するかという動きをカスタマイズできました。プロパティでは、これは出来ないのか?ということですが、PartCreationPolicy属性はクラスにつける属性なので、プロパティ単位で指定することは出来ません。ということで、クラスに以下のような感じで属性をつけてプログラムを再実行してみました。

// 共有しないでください!という意思を示してみた
[PartCreationPolicy(CreationPolicy.NonShared)]
public class Greeter
{
    public Greeter()
    {
        Console.WriteLine("Greeterのインスタンスが作られました");
    }

    // string型で挨拶という名前でプロパティをExport
    [Export("挨拶")]
    public string Greet
    {
        get
        {
            Console.WriteLine("プロパティから値が取得されました");
            return "こんにちは";
        }
    }
}

実行結果は以下のようになります。

Greeterのインスタンスが作られました
プロパティから値が取得されました
こんにちは
Greeterのインスタンスが作られました
プロパティから値が取得されました
こんにちは

毎回、Greeterクラスのインスタンスが作られる挙動に変わったのがわかると思います。とりあえず、このような動きをするということは把握しておきましょう。

使いどころは?

プロパティのExportって具体的にどんな時に使えるの?という疑問がわいてくると思います。私も最初そうでした。このプロパティを使ったExportを行うと、自分で属性をつけることのできないクラスのインスタンスをコンテナに登録できるといったメリットがあります。疑似的なコードですが、とっても汎用的に使える素敵なログライブラリのLoggerというクラスがあって、こいつをMEFに管理させたいと思ったとします。
ですが、Loggerクラスは、自分で作ったものでは無いのでExport属性をつけられません。

そんな時に以下のようにプロパティにExportを付ける方法だとコンテナに登録することが出来ます。

public class LoggerHolder
{
    private Logger logger;
    
    public LoggerHolder()
    {
        // loggerの初期化処理
    }
    
    // LoggerをMEFのコンテナに登録
    [Export]
    public Logger Logger
    {
        get
        {
            return this.logger;
        }
    }
}

結構強力な機能だと思うのでプロパティのExportは覚えておきましょう。因みに、説明していませんが、プロパティを通してExportしたオブジェクトも他のクラスにImportすることが出来ます。

メソッドのExport

次は、メソッドのExportについて説明します。メソッドのExportと来たとき、最初はメソッドの実行結果をExportするんだろ?と私は思ったりしたのですが、実は違います。これはメソッドをデリゲートとしてExportします。

これも小さな例を見て動きを理解してみましょう。

class Greeter
{
    [Export("挨拶")]
    public string Japanese()
    {
        return "こんにちは";
    }

    [Export("挨拶")]
    public string English()
    {
        return "Hello";
    }
}

プロパティの時と同じようにメソッドをExportしています。こうすると、引数を受け取らずにstringの戻り値を返すデリゲートとしてメソッドがExportされます。Importする側もデリゲートとしてImportします。今回の例では引数無しでstringを返すのでFunc型あたりで受け取ることが出来ます。

では、これをインポートするクラスを作ってみようと思います。挨拶という名前で複数ExportされているのでImportManyを使ってImportします。

[Export]
class Application
{
    // 引数無しでstringを返して挨拶という名前でExportされている
    // メソッドをImportする
    [ImportMany("挨拶")]
    public IEnumerable<Func<string>> GreetMessageGenerators { get; set; }

    public void Run()
    {
        // Importしたメソッドを呼び出す
        Console.WriteLine("Application start.");
        foreach (var g in this.GreetMessageGenerators)
        {
            Console.WriteLine(g());
        }
        Console.WriteLine("Application end.");
    }
}

Importする側のクラスも出来たのでMainメソッドを作って動きを確認します。

static void Main(string[] args)
{
    // 手慣れたコンテナの作成
    var container = new CompositionContainer(
        new AssemblyCatalog(Assembly.GetExecutingAssembly()));

    // Applicationクラスをコンテナから取得して実行
    var app = container.GetExportedValue<Application>();
    app.Run();
}

実行結果は以下のようになります。

Application start.
こんにちは
Hello
Application end.

ちゃんとメソッドが呼び出されています。このように単純な処理をExportしたりImportするだけなら、わざわざインターフェースを切らなくてもメソッドをExportしてImportすることで事足りてしまいます。実は、前回の拡張可能なアプリケーションとか、それの最たる例ですね。

まとめ

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

  • プロパティをExportすることが出来る
    • クラスにPartCreationPolicyを付けることで取得されるインスタンスを再利用するか、毎回取得するか制御できる
  • メソッドをExportすることが出来る
    • メソッドはデリゲートとしてExportされる
    • Importする側はFuncやActionなどを使って受け取る

以上、個人的にMEFの特徴的な機能だと思ってるプロパティのExportとメソッドのExportの紹介でした。