Cでの拡張メソッドのクリエイティブな使用#

こんにちは、Habr!



C#トピックの調査を続けて、拡張メソッドの元の使用に関する次の短い記事を翻訳しました。インターフェイスに関する最後のセクションと、作成者のプロファイルに特に注意することをお勧めします







C#の経験が少しでもある人なら誰でも、拡張メソッドの存在を知っていると思います。これは、開発者が既存のタイプを新しいメソッドで拡張できる優れた機能です。



これは、制御できないタイプに機能を追加する場合に非常に便利です。実際、遅かれ早かれ、物事をよりアクセスしやすくするために、BCLの拡張機能を作成する必要がありました。



しかし、比較的明白な使用例に加えて、拡張メソッドの使用に直接関連する非常に興味深いパターンもあり、それらを従来とは異なる方法で使用する方法を示しています。



列挙へのメソッドの追加



列挙は、それぞれが一意の名前を割り当てられた定数数値の単なるコレクションです。 C#の列挙は、抽象クラスEnumを継承しますが、実際のクラスとしては解釈されません。特に、この制限により、メソッドを使用できなくなります。



場合によっては、ロジックを列挙型にプログラムすると役立つことがあります。たとえば、列挙値が複数の異なるビューに存在する可能性があり、1つを別のビューに簡単に変換したい場合です。



たとえば、さまざまな形式でファイルを保存できる一般的なアプリケーションで、次のタイプを想像してみてください。



public enum FileFormat
{
    PlainText,
    OfficeWord,
    Markdown
}


この列挙は、アプリケーションでサポートされている形式のリストを定義し、アプリケーションのさまざまな部分で使用して、特定の値に基づいて分岐ロジックを開始できます。



各ファイル形式はファイル拡張子として表すことができるので、それぞれFileFormatにこの情報を取得する方法があると便利ですこれを行うことができるのは、次のような拡張メソッドを使用する場合です。



public static class FileFormatExtensions
{
    public static string GetFileExtension(this FileFormat self)
    {
        if (self == FileFormat.PlainText)
            return "txt";

        if (self == FileFormat.OfficeWord)
            return "docx";

        if (self == FileFormat.Markdown)
            return "md";

        //  ,      ,
        //      
        throw new ArgumentOutOfRangeException(nameof(self));
    }
}


これにより、次のことが可能になります。



var format = FileFormat.Markdown;
var fileExt = format.GetFileExtension(); // "md"
var fileName = $"output.{fileExt}"; // "output.md"


モデルクラスのリファクタリング



たとえば、アネミックモデルを使用している場合など、クラスにメソッドを直接追加したくない場合があります



アネミックモデルは通常、get-onlyのパブリック不変プロパティのセットで表されます。したがって、モデルクラスにメソッドを追加すると、コードの純度が侵害されているように見える場合や、メソッドが何らかのプライベート状態を参照していると思われる場合があります。拡張メソッドは、モデルのプライベートメンバーにアクセスできず、本質的にモデルの一部ではないため、この問題は発生しません。



したがって、2つのモデルを使用した次の例を検討してください。1つは閉じたタイトルリストを表し、もう1つは別のタイトル行を表します。



public class ClosedCaption
{
    //  
    public string Text { get; }

    //       
    public TimeSpan Offset { get; }

    //       
    public TimeSpan Duration { get; }

    public ClosedCaption(string text, TimeSpan offset, TimeSpan duration)
    {
        Text = text;
        Offset = offset;
        Duration = duration;
    }
}

public class ClosedCaptionTrack
{
    // ,    
    public string Language { get; }

    //   
    public IReadOnlyList<ClosedCaption> Captions { get; }

    public ClosedCaptionTrack(string language, IReadOnlyList<ClosedCaption> captions)
    {
        Language = language;
        Captions = captions;
    }
}


現在の状態では、特定の時間にサブタイトル文字列を表示する必要がある場合、次のようにLINQを実行します。



var time = TimeSpan.FromSeconds(67); // 1:07

var caption = track.Captions
    .FirstOrDefault(cc => cc.Offset <= time && cc.Offset + cc.Duration >= time);


これは、メンバーメソッドまたは拡張メソッドのいずれかとして実装できるある種のヘルパーメソッドを実際に要求します。私は2番目のオプションを好みます。



public static class ClosedCaptionTrackExtensions
{
    public static ClosedCaption GetByTime(this ClosedCaptionTrack self, TimeSpan time) =>
        self.Captions.FirstOrDefault(cc => cc.Offset <= time && cc.Offset + cc.Duration >= time);
}


この場合、拡張方法を使用すると、通常の方法と同じことを実現できますが、多くの非自明なボーナスが得られます。



  1. このメソッドがクラスのパブリックメンバーでのみ機能し、そのプライベート状態を不思議に変更しないことは明らかです。
  2. 明らかに、この方法は単に角を切ることを可能にし、便宜上ここに提供されています。
  3. このメソッドは、データをロジックから分離することを目的とした完全に別個のクラス(またはアセンブリ)に属しています。


一般に、拡張方式のアプローチを使用する場合、必要なものと有用なものの間に線を引くと便利です。



インターフェースを多用途に



するインターフェースを設計するときは、実装が容易になるため、常に契約をできるだけ小さくする必要があります。インターフェイスが最も一般的な方法で機能を提供する場合に非常に役立ちます。これにより、同僚(または自分自身)がその上に構築して、より具体的なケースを処理できます。



これが意味をなさないように聞こえる場合は、モデルをファイルに保存する一般的なインターフェイスを検討してください。



public interface IExportService
{
    FileInfo SaveToFile(Model model, string filePath);
}


すべて正常に動作しますが、数週間以内に新しい要件が発生する可能性があります。実装するクラスはIExportService、ファイルにエクスポートできるだけでなく、ファイルに書き込むこともできる必要があります。



したがって、この要件を満たすために、契約に新しいメソッドを追加します。



public interface IExportService
{
    FileInfo SaveToFile(Model model, string filePath);

    byte[] SaveToMemory(Model model);
}


この変更により、既存のすべての実装が壊れIExportServiceました。メモリへの書き込みもサポートするように、すべてを更新する必要があるためです。



しかし、これをすべて行わないために、最初とは少し異なる方法でインターフェイスを設計することもできます。



public interface IExportService
{
    void Save(Model model, Stream output);
}


この形式では、インターフェイスにより、宛先を最も一般化された形式、つまりthisで記述する必要がありますStream。これで、作業中のファイルに制限されなくなり、他のさまざまな出力オプションをターゲットにすることもできます。



このアプローチの唯一の欠点は、最も基本的な操作が以前ほど単純ではないことです。特定のインスタンスを設定し、Streamそれをusingステートメントでラップして、パラメーターとして渡す必要があります。



幸い、拡張メソッドを使用すると、この欠点は完全に無効になります。



public static class ExportServiceExtensions
{
    public static FileInfo SaveToFile(this IExportService self, Model model, string filePath)
    {
        using (var output = File.Create(filePath))
        {
            self.Save(model, output);
            return new FileInfo(filePath);
        }
    }

    public static byte[] SaveToMemory(this IExportService self, Model model)
    {
        using (var output = new MemoryStream())
        {
            self.Save(model, output);
            return output.ToArray();
        }
    }
}


元のインターフェースをリファクタリングすることで、それをはるかに用途の広いものにし、拡張メソッドを使用して使いやすさを犠牲にすることはありませんでした。



したがって、拡張メソッドは、単純なものを単純保ち、複雑なものを可能なものに変えるための非常に貴重なツールだと思います



All Articles