Metoda rozšíření - Extension method

V objektově orientovaného programování , An způsob rozšíření je metoda přidá k objektu poté, co původní objekt byl sestaven . Upraveným objektem je často třída, prototyp nebo typ. Metody rozšíření jsou rysy některých objektově orientovaných programovacích jazyků. Mezi voláním metody rozšíření a voláním metody deklarované v definici typu není žádný syntaktický rozdíl.

Ne všechny jazyky však implementují metody rozšíření stejně bezpečným způsobem. Například jazyky jako C #, Java (přes Manifold) a Kotlin nijak nemění rozšířenou třídu, protože by to mohlo narušit hierarchie tříd a narušit odesílání virtuálních metod. To je důvod, proč tyto jazyky striktně implementují metody rozšíření staticky a k jejich vyvolání používají statické dispečink .

Podpora v programovacích jazycích

Metody rozšíření jsou rysy mnoha jazyků, včetně C # , Java přes Manifold , Gosu , JavaScript , Oxygene , Ruby , Smalltalk , Kotlin , Dart , Visual Basic.NET a Xojo . V dynamických jazycích, jako je Python , je koncept metody rozšíření zbytečný, protože třídy lze rozšířit bez jakékoli speciální syntaxe (přístup známý jako „monkey-patching“, používaný v knihovnách, jako je gevent ).

Ve VB.NET a Oxygene jsou rozpoznány přítomností extensionklíčového slova nebo atributu. V Xojo se Extendsklíčové slovo " " používá s globálními metodami.

V C # jsou implementovány jako statické metody ve statických třídách, přičemž první argument je rozšířené třídy a předchází thisklíčové slovo „ “.

V Javě přidáváte metody rozšíření přes Manifold , soubor jar, který přidáte do třídy projektu vašeho projektu. Podobně jako v C # je metoda rozšíření Java deklarována statická ve třídě @Extension, kde první argument má stejný typ jako rozšířená třída a je opatřen poznámkami @This.

V Smalltalk může libovolný kód kdykoli přidat metodu do jakékoli třídy odesláním zprávy o vytvoření metody (například methodsFor:) do třídy, kterou chce uživatel rozšířit. Kategorie metody Smalltalk je konvenčně pojmenována po balíčku, který poskytuje rozšíření, obklopená hvězdičkami. Například když kód aplikace Etoys rozšiřuje třídy v základní knihovně, přidané metody se vloží do *etoys*kategorie.

V Ruby, jako Smalltalk, neexistuje žádná speciální funkce jazyka pro rozšíření, protože Ruby umožňuje kdykoli znovu otevřít třídy pomocí classklíčového slova, v tomto případě přidat nové metody. Komunita Ruby často popisuje metodu rozšíření jako jakýsi opičí patch . K dispozici je také novější funkce pro přidání bezpečných / místních rozšíření k objektům, nazývaná Vylepšení , ale je známo, že je méně používána.

V Swift extensionklíčové slovo označuje konstrukci podobnou třídě, která umožňuje přidání metod, konstruktorů a polí do existující třídy, včetně možnosti implementovat nové rozhraní / protokol do existující třídy.

Metody rozšíření jako povolovací funkce

Kromě metod rozšíření, které umožňují rozšířit kód napsaný ostatními, jak je popsáno níže, umožňují metody rozšíření také vzory, které jsou samy o sobě užitečné. Hlavním důvodem, proč byly zavedeny metody rozšíření, byl Language Integrated Query (LINQ). Podpora kompilátoru pro metody rozšíření umožňuje hlubokou integraci LINQ se starým kódem stejně jako s novým kódem, stejně jako podporu syntaxe dotazu, která je v tuto chvíli pro primární jazyky Microsoft .NET jedinečná .

Console.WriteLine(new[] { Math.PI, Math.E }.Where(d => d > 3).Select(d => Math.Sin(d / 2)).Sum());
// Output:
// 1

Centralizujte běžné chování

Metody rozšíření však umožňují implementovat funkce jednou způsoby, které umožňují opětovné použití bez nutnosti dědičnosti nebo režie vyvolávání virtuálních metod , nebo vyžadují, aby implementátoři rozhraní implementovali buď triviální, nebo žalostně složitou funkci.

Obzvláště užitečný scénář je, pokud funkce funguje na rozhraní, pro které neexistuje žádná konkrétní implementace, nebo autor implementace knihovny tříd neposkytne užitečnou implementaci, např. Jak tomu často bývá v knihovnách, které vývojářům poskytují architekturu zásuvných modulů nebo podobné funkce. .

Zvažte následující kód a předpokládejme, že je to jediný kód obsažený v knihovně tříd. Nicméně každý implementátor rozhraní ILogger získá schopnost zapsat formátovaný řetězec pouhým zahrnutím příkazu MyCoolLogger , aniž by jej musel implementovat jednou a bez nutnosti podtřídy knihovny tříd za předpokladu implementace ILogger.

namespace MyCoolLogger {
    public interface ILogger { void Write(string text); }
    public static class LoggerExtensions {
        public static void Write(this ILogger logger, string format, params object[] args) { 
            if (logger != null)
                logger.Write(string.Format(format, args));
        }
    }
}
  • použít jako :
    var logger = new MyLoggerImplementation();
    logger.Write("{0}: {1}", "kiddo sais", "Mam mam mam mam ...");
    logger.Write("{0}: {1}", "kiddo sais", "Ma ma ma ma... ");
    logger.Write("{0}: {1}", "kiddo sais", "Mama mama mama mama ");
    logger.Write("{0}: {1}", "kiddo sais", "Mamma mamma mamma ... ");
    logger.Write("{0}: {1}", "kiddo sais", "Elisabeth Lizzy Liz...");
    logger.Write("{0}: {1}", "mamma sais", "WHAT?!?!!!");
    logger.Write("{0}: {1}", "kiddo sais", "hi.");
    

Lepší volná spojka

Metody rozšíření umožňují uživatelům knihoven tříd upustit od deklarování argumentu, proměnné nebo čehokoli jiného s typem, který pochází z této knihovny. Konstrukci a převod typů použitých v knihovně tříd lze implementovat jako rozšiřující metody. Po pečlivé implementaci převodů a továren lze přechod z jedné knihovny tříd do jiné usnadnit stejně jako změnit příkaz using, který zpřístupní metody rozšíření, ke kterým se kompilátor váže.

Plynulá rozhraní programátoru aplikací

Metody rozšíření mají speciální použití při implementaci takzvaných plynulých rozhraní. Příkladem je konfigurační rozhraní API Entity Framework společnosti Microsoft, které umožňuje například psát kód, který se podobá běžné angličtině tak blízko, jak je to praktické.

Dalo by se namítnout, že je to stejně dobře možné bez rozšiřujících metod, ale zjistíme, že v praxi poskytují rozšiřující metody lepší zážitek, protože na hierarchii tříd jsou kladena menší omezení, aby fungovala - a čtela - podle potřeby.

Následující příklad používá Entity Framework a konfiguruje třídu TodoList, která má být uložena v databázové tabulce Seznamy a definuje primární a cizí klíč. Kód by měl být chápán víceméně jako: „TodoList má klíčový TodoListID, jeho název sady entit je Seznamy a má mnoho TodoItem, z nichž každý má požadovaný TodoList.“

public class TodoItemContext : DbContext 
{
    public DbSet<TodoItem> TodoItems { get; set; }
    public DbSet<TodoList> TodoLists { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder) 
    {
        base.OnModelCreating(modelBuilder);
        modelBuilder.Entity<TodoList>()
                    .HasKey(e => e.TodoListId)
                    .HasEntitySetName("Lists")
                    .HasMany(e => e.Todos)
                    .WithRequired(e => e.TodoList);
    }
}

Produktivita

Zvažte například IEnumerable a všimněte si jeho jednoduchosti - existuje pouze jedna metoda, přesto je to víceméně základ LINQ. V Microsoft .NET existuje mnoho implementací tohoto rozhraní. Samozřejmě by bylo obtížné vyžadovat, aby každá z těchto implementací implementovala celou řadu metod, které jsou definovány v oboru názvů System.Linq, aby fungovaly na IEnumerables, přestože Microsoft má veškerý zdrojový kód. Ještě horší je, že by to vyžadovalo, aby všichni kromě Microsoftu, kteří uvažují o použití IEnumerable, také implementovali všechny tyto metody, což by bylo velmi antiproduktivní vzhledem k širokému používání tohoto velmi běžného rozhraní. Místo toho implementací jedné metody tohoto rozhraní lze LINQ použít víceméně okamžitě. Zejména v prakticky většině případů je metoda GetEnumerator IEnumerable delegována na implementaci GetEnumerator soukromé kolekce, seznamu nebo pole.

public class BankAccount : IEnumerable<decimal> 
{
    private List<Tuple<DateTime, decimal>> credits; // assumed all negative
    private List<Tuple<DateTime, decimal>> debits; // assumed all positive

    public IEnumerator<decimal> GetEnumerator() 
    {
        var query = from dc in debits.Union(credits) 
                    orderby dc.Item1 /* Date */ 
                    select dc.Item2; /* Amount */
    
        foreach (var amount in query)
            yield return amount;
    }
}
// given an instance of BankAccount called ba and a using System.Linq on top of the current file,
// one could now write ba.Sum() to get the account balance, ba.Reverse() to see most recent transactions first,
// ba.Average() to get the average amount per transaction, etcetera - without ever writing down an arithmetic operator

Výkon

To znamená, že lze přidat další implementace funkce poskytované metodou rozšíření za účelem zlepšení výkonu nebo řešení odlišně implementovaných implementací rozhraní, jako je poskytnutí kompilátoru implementace IEnumerable konkrétně pro pole (v System.SZArrayHelper), kterou automaticky zvolí volání metody rozšíření u odkazů zadaných do pole, protože jejich argument bude konkrétnější (tato hodnota T []) než metoda rozšíření se stejným názvem, která funguje na instancích rozhraní IEnumerable (tato hodnota IEnumerable).

Zmírnění potřeby společné základní třídy

S obecnými třídami umožňují metody rozšíření implementaci chování, které je k dispozici pro všechny instance obecného typu, aniž by se vyžadovalo, aby byly odvozeny ze společné základní třídy, a bez omezení parametrů typu na konkrétní větev dědičnosti. To je velká výhra, protože situace, kdy tento argument platí, vyžadují negenerickou základní třídu jen pro implementaci sdílené funkce - která pak vyžaduje, aby obecná podtřída prováděla box a / nebo sesílala, kdykoli je použitý typ jedním z argumentů typu .

Konzervativní použití

Měla by být uvedena poznámka o upřednostňování metod rozšiřování před jinými prostředky k dosažení opětovného použití a správného objektově orientovaného designu. Metody rozšíření mohou „zaplnit“ funkce automatického dokončování editorů kódu, jako je IntelliSense sady Visual Studio, proto by měly být buď ve svém vlastním oboru názvů, aby je vývojář mohl selektivně importovat, nebo by měly být definovány pro typ, který je dostatečně specifický pro metoda, která se zobrazí v IntelliSense, pouze pokud je skutečně relevantní a vzhledem k výše uvedenému, zvažte, že by mohlo být těžké je najít, pokud by je vývojář očekával, ale chybí jim z IntelliSense kvůli chybějícímu příkazu using, protože vývojář nemusí mít přidruženou metodu s třídou, která ji definuje, nebo dokonce s jmenným prostorem, ve kterém žije - ale spíše s typem, který rozšiřuje, a s jmenným prostorem, ve kterém žije.

Problém

V programování vznikají situace, kdy je nutné přidat funkčnost do existující třídy - například přidáním nové metody. Normálně by programátor upravil zdrojový kód existující třídy , ale to programátora donutí překompilovat všechny binární soubory s těmito novými změnami a vyžaduje, aby programátor mohl změnit třídu, což není vždy možné, například při použití tříd ze třetí -strana shromáždění . K tomu obvykle dochází jedním ze tří způsobů, které jsou poněkud omezené a neintuitivní:

  1. Zdědit třídu a poté implementovat funkce v instanční metodě v odvozené třídě.
  2. Implementujte funkčnost do statické metody přidané do třídy pomocníka.
  3. Místo dědičnosti použijte agregaci .

Aktuální řešení C #

První možnost je v zásadě jednodušší, ale je bohužel omezena skutečností, že mnoho tříd omezuje dědičnost určitých členů nebo jej úplně zakazuje. To zahrnuje zapečetěnou třídu a různé primitivní datové typy v C #, například int , float a string . Druhá možnost na druhé straně tato omezení nesdílí, ale může být méně intuitivní, protože vyžaduje použití odkazu na samostatnou třídu namísto přímého použití metod dané třídy.

Jako příklad zvažte potřebu rozšíření třídy řetězce o novou reverzní metodu, jejíž návratovou hodnotou je řetězec se znaky v obráceném pořadí. Protože třída řetězce je uzavřený typ, metoda by se obvykle přidala do nové třídy nástrojů podobným způsobem jako následující:

string x = "some string value";
string y = Utility.Reverse(x);

To však může být stále obtížnější orientovat se, jak se zvyšuje knihovna metod a tříd nástrojů, zejména pro nováčky. Umístění je také méně intuitivní, protože na rozdíl od většiny řetězcových metod by nebylo členem třídy řetězců, ale v úplně jiné třídě. Lepší syntaxe by tedy byla následující:

string x = "some string value";
string y = x.Reverse();

Aktuální řešení VB.NET

Ve většině způsobů je řešení VB.NET podobné výše uvedenému řešení C #. VB.NET má však jedinečnou výhodu v tom, že umožňuje předávání členů do rozšíření odkazem (C # umožňuje pouze podle hodnoty). S ohledem na následující;

Dim x As String = "some string value"
x.Reverse()

Protože Visual Basic umožňuje předání zdrojového objektu odkazem, je možné provádět změny zdrojového objektu přímo, aniž byste museli vytvářet další proměnné. Je také intuitivnější, protože funguje konzistentně se stávajícími metodami tříd.

Metody rozšíření

Nová jazyková funkce metod rozšíření v C # 3.0 však umožňuje tento druhý kód. Tento přístup vyžaduje statickou třídu a statickou metodu následujícím způsobem.

public static class Utility
{
    public static string Reverse(this string input)
    {
        char[] chars = input.ToCharArray();
        Array.Reverse(chars);
        return new String(chars);
    }
}

V definici modifikátor „this“ před prvním argumentem určuje, že se jedná o metodu rozšíření (v tomto případě na typ „string“). Ve volání není první argument „předán“, protože je již známý jako „volající“ objekt (objekt před tečkou).

Hlavní rozdíl mezi voláním metod rozšíření a voláním statických pomocných metod spočívá v tom, že statické metody jsou volány v notaci předpony , zatímco metody rozšíření jsou volány v notaci infix . Druhá možnost vede k čitelnějšímu kódu, když je výsledek jedné operace použit pro jinou operaci.

Statickými metodami
HelperClass.Operation2(HelperClass.Operation1(x, arg1), arg2)
S metodami rozšíření
x.Operation1(arg1).Operation2(arg2)

Konflikty pojmenování v metodách rozšíření a metodách instance

V C # 3.0 může pro třídu existovat jak metoda instance, tak metoda rozšíření se stejným podpisem. V takovém scénáři je upřednostňována metoda instance před metodou rozšíření. Kompilátor ani Microsoft Visual Studio IDE nevarují před konfliktem názvů. Zvažte tuto třídu C #, kde je GetAlphabet()metoda vyvolána na instanci této třídy:

class AlphabetMaker 
{
    public void GetAlphabet()       
    {                               //When this method is implemented,
        Console.WriteLine("abc");   //it will shadow the implementation
    }                               //in the ExtensionMethods class.
}

static class ExtensionMethods
{
    public static void GetAlphabet(this AlphabetMaker am)   
    {                               //This will only be called                       
        Console.WriteLine("ABC");   //if there is no instance
    }                               //method with the same signature.   
}

Výsledek vyvolání GetAlphabet()instance, AlphabetMakerpokud existuje pouze metoda rozšíření:

ABC

Výsledek, pokud existuje metoda instance i metoda rozšíření:

abc

Viz také

Reference

externí odkazy