Uso di lambda per migliorare l'incapsulamento

6

Proprio come molte persone credono tenacemente in piccole funzioni , alcune persone credono che lambda debba contenere solo frammenti di codice ridotto.

Un vantaggio spesso trascurato di lambda tuttavia, se li stai utilizzando puoi incapsulare un comportamento che altrimenti dovresti rendere disponibile all'intera classe .

Non è un vero vantaggio mantenere l'incapsulamento corretto, indipendentemente dal conteggio delle righe? Quali sono i possibili svantaggi dell'uso di funzioni anonime con molte righe di codice che sto trascurando?

Quello che segue è un esempio reale. argumentsMatch esegue una corrispondenza argomento molto specifica, strongmente dipendente dal comportamento della funzione in cui è definita. Il seguente codice IMHO segue il principio di singola responsabilità . Lo spostamento di argumentsMatch in un metodo privato comporterebbe che venisse richiamato solo da questo metodo.

/// <summary>
///   Get the first found matching generic type.
///   The type parameters of the generic type are optional.
///   E.g. Dictionary<,>
///   When full (generic) type is known (e.g. Dictionary<string,string>),
///   the "is" operator is most likely more performant,
///   but this function will still work correctly.
/// </summary>
/// <param name = "source">The source for this extension method.</param>
/// <param name = "type">The type to check for.</param>
/// <returns>
///   The first found matching complete generic type,
///   or null when no matching type found.
/// </returns>
public static Type GetMatchingGenericType( this Type source, Type type )
{
    Type[] genericArguments = type.GetGenericArguments();
    Type rawType = type.IsGenericType ? type.GetGenericTypeDefinition() : type;

    // Used to compare type arguments and see whether they match.
    Func<Type[], bool> argumentsMatch
        = arguments => genericArguments
            .Zip( arguments, Tuple.Create )
            .All( t => t.Item1.IsGenericParameter || // No type specified.
                       t.Item1 == t.Item2 );

    Type matchingType = null;
    if ( type.IsInterface )
    {
        // Traverse across all interfaces to find a matching interface.
        matchingType = 
            (from t in source.GetInterfaces()
             let rawInterface = t.IsGenericType ? t.GetGenericTypeDefinition() : t
             where rawInterface == rawType &&
                   argumentsMatch( t.GetGenericArguments() )
             select t).FirstOrDefault();
    }
    else
    {
        // Traverse across the type, and all it's base types.
        Type baseType = source;
        while ( baseType != null && baseType != typeof( object ) )
        {
            Type rawCurrent = baseType.IsGenericType
                ? baseType.GetGenericTypeDefinition()
                : baseType;
            if ( rawType == rawCurrent )
            {
                // Same raw generic type, compare type arguments.
                if ( argumentsMatch( baseType.GetGenericArguments() ) )
                {
                    matchingType = baseType;
                    break;
                }
            }
            baseType = baseType.BaseType;
        }
    }

    return matchingType;
}
    
posta Steven Jeuris 05.06.2011 - 19:43
fonte

5 risposte

6

Penso che il punto importante qui sia la località del codice: i codici che dovrebbero essere letti insieme dovrebbero essere vicini.

La mia regola generale è: se sia la funzione chiamata (lambda) che il chiamante possono essere letti, compresi, mantenuti, testati (e possibilmente, ma non necessariamente riutilizzati) separatamente, quindi estrarre la funzione lambda in una funzione separata o classe. Se i due sono così strettamente legati insieme da non poter davvero dare un senso a uno senza sapere cosa fa l'altro, quindi tenerli il più vicino possibile; lambda sono perfetti per questo, se la lingua che usi non consente funzioni annidate.

    
risposta data 06.06.2011 - 10:44
fonte
3

Penso che sia più dipendente dal contesto. In alcune lingue lambda o funzioni in generale non sono un'alternativa per l'incapsulamento, sono la struttura principale per l'incapsulamento. In altre lingue questa tecnica viene spesso trascurata a favore di strutture più idiomatiche.

Alcuni problemi nell'uso di lambda per l'incapsulamento:

  • Potrebbe essere più difficile mantenere il codice perché il codice non è univoco. Pertanto, ogni volta che uno sviluppatore incontra la costruzione del codice, probabilmente avrà bisogno di almeno un ulteriore tempo per capire il codice. Tuttavia, possono anche avere solo una comprensione parziale di ciò che fa il costrutto (o cosa significa in un contesto più ampio).

Ad esempio: ho lavorato con un team di sviluppatori in cui le chiusure ei thunk non sono stati utilizzati o compresi bene. L'ho scoperto dopo che un codice di chiusura che ho scritto è stato modificato in un modo che ha rotto la funzionalità. Ho anche ricevuto alcune lamentele sul fatto che il lambdas abbia complicato il codice, tuttavia penso che questo sia stato più il risultato di non capire quali sono state le chiusure piuttosto che i codici complessi.

  • Un altro problema è quello di accoppiarsi a un particolare periodo di esecuzione durante un processo. Soprattutto se è necessario modificarlo in seguito o fornire questa funzionalità in un'altra posizione.

Ad esempio: quando ero più giovane e volevo essere intelligente, definivo il codice in un lambda autodiagnostico. In seguito mi imbatterei in un problema in cui dovevo dare a quel codice l'accesso alle risorse che erano state definite in seguito (handle di database, socket, ecc.). Poiché il codice è stato eseguito immediatamente, tutte le connessioni sono state create in anticipo. È stato difficile modificare questo codice per ritardare l'inizializzazione della connessione in seguito perché sia il lambda autoesecutivo, sia il codice circostante facevano presupposizioni basate sul tempo dell'inizializzazione, es. accoppiamento temporale. Ho finito per dover eseguire il curry del lambda per ritardarne l'esecuzione fino a un momento successivo.

    
risposta data 05.06.2011 - 20:41
fonte
3

In C # 7.0, le funzioni locali saranno introdotto che ha lo stesso risultato delineato nella domanda, ma con una sintassi molto migliorata.

Sometimes a helper function only makes sense inside of a single method that uses it. You can now declare such functions inside other function bodies as a local function:

public int Fibonacci(int x)
{
    if (x < 0) throw new ArgumentException("Less negativity please!", nameof(x));
    return Fib(x).current;

    (int current, int previous) Fib(int i)
    {
        if (i == 0) return (1, 0);
        var (p, pp) = Fib(i - 1);
        return (p + pp, p);
    }
}
    
risposta data 31.08.2016 - 13:45
fonte
2

È anche un grande vantaggio scrivere in linea le funzioni lambda, e questo rimane vero indipendentemente dalle dimensioni lambda. Mi scusi, sto usando C ++ qui, non C #, ma si applicano gli stessi principi.

Secondo me, le persone che sono contro i lambda lunghi lo stanno semplicemente guardando male. Lo scopo di un lambda è che il codice può essere riutilizzato come una variabile. Considera cosa succederebbe se tu stessi parlando di memorizzare int come variabile.

void method() {
    static const int num_clients = 65535;
    Socket sockets[num_clients];
    for(int i = 0; i < num_clients; i++) {
        do_something_with_socket(sockets[i]);
    }
}

Questo va bene e bene, indipendentemente da quante linee ci vuole per inizare num_clients . Devo avere qualche tipo di contatore o vantaggio LoC per giustificare il fatto di non ripetermi? Ovviamente no. Lambda sono allo stesso modo.

void method() {
    auto lambda = [](int x) { std::cout << x; }
    lambda(1);
    int some_int;
    std::cin >> some_int;
    lambda(some_int);
}

Importa quante linee lambda è? Certo che no ... non mi sono ripetuto, non importa quanti LoC non ho ripetuto. Ma se questo codice non è mai necessario al di fuori di questa funzione, allora dovrebbe rimanere in questa funzione - specialmente se lambda usa le variabili catturate. Dovrebbero diventare variabili membro da utilizzare in una singola funzione? Ovviamente no. Devo passare manualmente in giro? Spero di non aver voluto catturare più di un paio di variabili, o questo diventerà un vero casino.

È piuttosto semplice. Perché il codice dovrebbe essere accessibile al resto della classe solo perché ho bisogno di riutilizzarlo in una singola funzione? Questa è solo una logica scadente. Chiunque pensi che i lambda debbano essere brevi non ha in realtà pensato alla logica della situazione ed è bloccato nella propria opinione soggettiva predefinita su cosa siano i lambda e per cosa sono utili, perché non c'è no motivo logico oggettivo che dovrebbero essere brevi.

Modifica: Intendo davvero, a breve come in, nessuna ragione per cui un metodo anonimo dovrebbe essere breve, rispetto a un metodo normale. Non sto cercando di suggerire o suggerire qualcosa sulla lunghezza dei metodi, in generale.

Modifica: questo è particolarmente negativo in linguaggi come C #, in cui ogni funzione deve far parte di una classe e non è sufficiente crearne una rapida in ambito namespace.

Ecco un esempio migliore.

WIN32_FIND_DATA FileData;
auto ProcessRenderLibrary = [&](){
    HMODULE module = LoadLibrary(FileData.cFileName);
    void* ptr = GetProcAddress(module, "CreateRender");
    if (ptr) {
        RendererInfo RI = RendererInfo(module, (RendererCreateFunction)ptr, FileData.cFileName);
        std::wstring text = L"I found a renderer, and it is " + RI.name;
        LogText(text);
        Renderers.emplace_back(std::move(RI));
    } else {
        FreeLibrary(module);
    }
};
HANDLE first = FindFirstFile(L"*Render.dll", &FileData);
ProcessRenderLibrary();
while(FindNextFile(first, &FileData)) {
    ProcessRenderLibrary();
}
FindClose(first);
    
risposta data 05.06.2011 - 20:39
fonte
1

Il problema di base nell'esempio non è come organizzare il codice all'interno di una classe. È piuttosto il fatto che violi il principio di responsabilità singola .

Se ritenessi che il metodo di analisi appartenesse alla responsabilità della classe, non ti dispiacerebbe che fosse un metodo (probabilmente privato).
Più in generale: se ritenete che una routine non dovrebbe essere disponibile per un'intera classe, fatela del tutto fuori dalla classe. Altrimenti, lascia che sia un membro della classe.

Nascondere la violazione in una lambda potrebbe avere un leggero vantaggio, ma è un po 'come folding regions . Nascondi un'organizzazione povera invece di ristrutturare.

    
risposta data 05.06.2011 - 20:17
fonte

Leggi altre domande sui tag