Kreativer Einsatz von Erweiterungsmethoden in C #

Hallo Habr!



Wir setzen unsere Erforschung des C # -Themas fort und haben für Sie den folgenden kurzen Artikel über die ursprüngliche Verwendung von Erweiterungsmethoden übersetzt. Wir empfehlen Ihnen, den letzten Abschnitt über Schnittstellen sowie das Profil des Autors besonders zu beachten .







Ich bin sicher, dass jeder mit ein wenig C # -Erfahrung die Existenz von Erweiterungsmethoden kennt. Dies ist eine nette Funktion, mit der Entwickler vorhandene Typen mit neuen Methoden erweitern können.



Dies ist äußerst praktisch, wenn Sie Typen, die Sie nicht steuern, Funktionen hinzufügen möchten. Tatsächlich musste jeder früher oder später eine Erweiterung für die BCL schreiben, um die Dinge zugänglicher zu machen.



Neben relativ offensichtlichen Anwendungsfällen gibt es aber auch sehr interessante Muster, die direkt mit der Verwendung von Erweiterungsmethoden verbunden sind und zeigen, wie sie auf nicht traditionelle Weise verwendet werden können.



Hinzufügen von Methoden zu Aufzählungen



Eine Aufzählung ist einfach eine Sammlung konstanter numerischer Werte, denen jeweils ein eindeutiger Name zugewiesen wird. Obwohl Aufzählungen in C # von der abstrakten Klasse Enum erben, werden sie nicht als echte Klassen interpretiert. Diese Einschränkung verhindert insbesondere, dass sie über Methoden verfügen.



In einigen Fällen kann es hilfreich sein, die Logik in eine Aufzählung zu programmieren. Zum Beispiel, wenn ein Aufzählungswert in mehreren verschiedenen Ansichten vorhanden sein kann und Sie einfach einen in einen anderen konvertieren möchten.



Stellen Sie sich beispielsweise den folgenden Typ in einer typischen Anwendung vor, mit der Sie Dateien in verschiedenen Formaten speichern können:



public enum FileFormat
{
    PlainText,
    OfficeWord,
    Markdown
}


Diese Aufzählung definiert eine Liste der von der Anwendung unterstützten Formate und kann in verschiedenen Teilen der Anwendung verwendet werden, um eine Verzweigungslogik basierend auf einem bestimmten Wert zu initiieren.



Da jedes Dateiformat als Dateierweiterung dargestellt werden kann, wäre es schön, wenn jedes FileFormateine Methode zum Abrufen dieser Informationen hätte. Mit der Erweiterungsmethode kann dies in etwa so erfolgen:



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));
    }
}


Was uns wiederum erlaubt, dies zu tun:



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


Refactoring von Modellklassen



Es gibt Zeiten, in denen Sie einer Klasse keine Methode direkt hinzufügen möchten, z. B. wenn Sie mit einem anämischen Modell arbeiten .



Anämische Modelle werden normalerweise durch eine Reihe von öffentlichen unveränderlichen Eigenschaften dargestellt, die nur verfügbar sind. Wenn Sie einer Modellklasse Methoden hinzufügen, entsteht möglicherweise der Eindruck, dass die Reinheit des Codes verletzt wird, oder Sie vermuten, dass sich die Methoden auf einen privaten Status beziehen. Erweiterungsmethoden verursachen dieses Problem nicht, da sie keinen Zugriff auf die privaten Mitglieder des Modells haben und von Natur aus nicht Teil des Modells sind.



Betrachten Sie das folgende Beispiel mit zwei Modellen, von denen eines eine geschlossene Titelliste und das andere eine separate Titelzeile darstellt:



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;
    }
}


Wenn im aktuellen Status die Untertitelzeichenfolge zu einem bestimmten Zeitpunkt angezeigt werden soll, führen wir LINQ wie folgt aus:



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

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


Dies erfordert wirklich eine Art Hilfsmethode, die entweder als Mitgliedsmethode oder als Erweiterungsmethode implementiert werden kann. Ich bevorzuge die zweite Option.



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);
}


In diesem Fall können Sie mit der Erweiterungsmethode das Gleiche wie mit der üblichen Methode erzielen, erhalten jedoch eine Reihe nicht offensichtlicher Boni:



  1. Es ist klar, dass diese Methode nur mit den öffentlichen Mitgliedern der Klasse funktioniert und ihren privaten Zustand nicht auf mysteriöse Weise ändert.
  2. Offensichtlich können Sie mit dieser Methode nur die Ecke abschneiden und werden hier nur zur Vereinfachung bereitgestellt.
  3. Diese Methode gehört zu einer vollständig separaten Klasse (oder sogar Assembly), deren Zweck darin besteht, Daten von der Logik zu trennen.


Im Allgemeinen ist es bei Verwendung des Ansatzes der Erweiterungsmethode zweckmäßig, eine Grenze zwischen notwendig und nützlich zu ziehen.



Vielseitige Schnittstellen



Wenn Sie eine Schnittstelle entwerfen, möchten Sie immer, dass der Vertrag so klein wie möglich gehalten wird, da dies die Implementierung erleichtert. Es ist sehr hilfreich, wenn die Benutzeroberfläche Funktionen auf allgemeinste Weise bereitstellt, sodass Ihre Kollegen (oder Sie selbst) darauf aufbauen können, um spezifischere Fälle zu behandeln.



Wenn dies für Sie unsinnig klingt, ziehen Sie eine typische Schnittstelle in Betracht, die ein Modell in einer Datei speichert:



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


Alles funktioniert gut, aber in ein paar Wochen kann eine neue Anforderung eintreten: Die implementierten Klassen IExportServicemüssen nicht nur in eine Datei exportieren, sondern auch in eine Datei schreiben können.



Um diese Anforderung zu erfüllen, fügen wir dem Vertrag eine neue Methode hinzu:



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

    byte[] SaveToMemory(Model model);
}


Diese Änderung hat nur alle vorhandenen Implementierungen zerstört IExportService, da sie jetzt alle aktualisiert werden müssen, um auch das Schreiben in den Speicher zu unterstützen.



Um dies alles nicht zu tun, hätten wir die Benutzeroberfläche von Anfang an etwas anders gestalten können:



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


In dieser Form Sie die Schnittstellenkräfte das Ziel in der allgemeinsten Form zu schreiben, die, das ist Stream. Jetzt sind wir beim Arbeiten nicht mehr auf Dateien beschränkt und können auch auf verschiedene andere Ausgabeoptionen abzielen.



Der einzige Nachteil dieses Ansatzes besteht darin, dass die grundlegendsten Operationen nicht so einfach sind, wie wir es gewohnt sind: Jetzt müssen Sie eine bestimmte Instanz festlegen Stream, sie in eine using-Anweisung einschließen und als Parameter übergeben.



Glücklicherweise wird dieser Nachteil bei Verwendung von Erweiterungsmethoden vollständig aufgehoben:



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();
        }
    }
}


Durch die Überarbeitung der ursprünglichen Benutzeroberfläche haben wir sie viel vielseitiger gestaltet und die Benutzerfreundlichkeit nicht durch die Verwendung von Erweiterungsmethoden beeinträchtigt.



Als solches finde ich Erweiterungsmethoden ein unschätzbares Werkzeug , um das Einfache einfach zu halten und den Komplex in das Mögliche zu verwandeln .



All Articles