Ciclo continuo senza valutazione dei dati due volte

6

Spesso mi imbatto nel seguente schema.

while(GetValue(i) != null)
{
    DoSomethingWith(GetValue(i++));
}

Qui GetValue viene eseguito due volte. Sarebbe molto più bello usare un modello in cui possiamo valutare E memorizzare il risultato di GetValue . In alcuni casi (flussi) è impossibile ottenere il valore due volte (vedere le alternative di seguito per soluzioni alternative). C'è qualche schema o costrutto di loop che possiamo usare?

Alcune alternative a cui ho pensato con i loro stessi svantaggi.

Alternativa 1

// Variable outside of loop scope, extra if
var value = null;
do
{
    value = GetValue(i++);
    if(value != null) { DoSomethingWith(value); }
} while(value != null);

Alternativa 2

// Two get values, variable outside of loop
var value = GetValue(i);
while(value != null)
{
  DoSometingWith(value);
  value = GetValue(++i);
}

Alternativa 3

// Out only works on reference types, most enumerables do not have
// a TryGet like method so we need to create our own wrapper
Object value;
while(TryGetValue(i++, out value))
{
    DoSomethingWith(value);
}

Scenario mondiale ideale (non valido C #)

while((var value = GetValue(i++)) != null)
{
    DoSomethingWith(value);
}
    
posta Roy T. 31.05.2017 - 13:45
fonte

7 risposte

12

Vicino alla soluzione "Mondo ideale"

Quanto segue è C # valido

public static void Main()
{
    var i = 0;
    string value;
    while ((value = GetValue(i++)) != null)
    {
        DoSomethingWith(value);
    }
}

private static string GetValue(int input)
{
    if (input > 20)
    {
        return null;
    }
    return input.ToString();
}

private static void DoSomethingWith(string value)
{
    Console.WriteLine(value);
}

Confronta con il tuo "scenario ideale del mondo":

var i = 0;
while((var value = GetValue(i++)) != null)
{
    DoSomethingWith(value);
}

Tutto quello che dovevo fare era tirare value fuori dal ciclo. Sembri disposto a farlo per tutte le alternative presentate. Quindi, non penso che sia troppo lungo.

"Infinito" Mentre alternativo

Ho giocato con le alternative ... eccone un'altra:

var i = 0;
while (true)
{
    var value = GetValue(i++);
    if (value == null)
    {
        break;
    }
    DoSomethingWith(value);
}

In questo caso puoi dichiarare value all'interno del ciclo (e usando var ), non devi controllare per due volte null , non devi chiamare GetValue due volte con lo stesso input e non devi creare un wrapper con un parametro out .

Alternativa utilizzando

Potremmo provare ad esprimere la stessa cosa usando un ciclo for . Anche se l'approccio ingenuo non funziona:

// I repeat, this does not work:
var i = 0;
for (string value = null; value != null; value = GetValue(i++))
{
    DoSomethingWith(value);
}

Il problema con questa versione è che inizia con value che è null , che soddisfa il criterio di uscita, e quindi non ottieni iterazioni.

Come JimmyJames sottolinea puoi scriverlo come in questo modo:

var i = 0;
for(string value; (value = GetValue(i++)) != null;)
{
    DoSomethingWith(value);
}

L'unico inconveniente che vedo è che devi scrivere il tipo (non puoi usare var ).

Addendum : questa è un'altra variante suggerito da Maliafo :

var i = 0;
for (var value = GetValue(i); value != null; value = GetValue(++i))
{
    DoSomethingWith(value);
}

Sebbene questa versione richieda di scrivere GetValue , non la chiamerà due volte con lo stesso valore.

Vuoi dichiarare anche i nello scope? Dai un'occhiata a questo:

for (var i = 0; ; i++)
{
    var value = GetValue(i);
    if (value == null)
    {
        break;
    }
    DoSomethingWith(value);
}

Questo è lo stesso ciclo che vediamo nell'infinito mentre la soluzione che ho postato sopra. Tuttavia, dal momento che non ho avuto alcuna condizione al momento ... perché non usare cambiarlo in for per incrementare i ?

Addendum : vedi anche la risposta di NetMage per un uso interessante delle funzionalità di C # 7.0 .

    
risposta data 31.05.2017 - 15:28
fonte
4

Utilizza una funzione di ordine superiore

Fai una sfogliata del libro di esercizi della programmazione funzionale: se hai una sequenza ricorrente nella struttura del tuo codice, crea una funzione che contenga le parti che rimangono uguali e passa le parti che variano come parametri di funzione (es. delegati, in C #). In questo modo, non importa quanto sia brutto, devi solo scriverlo una volta e nasconderlo in una libreria da qualche parte.

(Dovrai perdonarmi qui in Java, i miei C # skillz sono un po 'arrugginiti)

public static <T> void untilNull (Function<Integer,T> supplier, int i, Action<T>  action) {
    T v;
    while ((v = supplier.apply(i++)) != null)
        action.perform(v);
}

....
untilNull (this::getValue, 1, this::doSomethingWithValue);

Aggiorna - ora con versione C #:

public static <T> void UntilNull (Func<int,T> supplier, int i, Action<T> action) {
    T v;
    while ((v = supplier(i++)) != null) action(v);
}

....
UntilNull (this.GetValue, 1, this.DoSomethingWithValue);
    
risposta data 31.05.2017 - 23:20
fonte
4

In C # 7.0, possiamo solo (ab) usare è:

while (GetValue(i++) is var value && value != null) {
    DoSomethingWith(value);
}
    
risposta data 09.06.2017 - 02:21
fonte
3

La soluzione semplice

Questa soluzione è sicuramente migliore delle tue alternative 1 e 2 se hai bisogno di un codice minimo per risolvere il tuo problema

while(true){
    var value = GetValue(i++);
    if(value != null) { 
        DoSomethingWith(value); 
    }
    else{
        break;
    }
}

Soluzione compatibile con Foreach Iterator Based Foreach

A mio parere, per la migliore soluzione generale pulita, questo iteratore con parametro funzione soluzione basata funzionerebbe al meglio. Non c'è bisogno di classificare wrapper specifici e codice duplicato fornito dalla terza opzione e funziona per ogni problema come questo (quindi penso che sia ancora meglio delle implementazioni in loop while). E ancora meglio, se stai andando per gli iteratori comunque, perché non inserire il nullcheck nell'iteratore stesso? In questo modo puoi usare questo per quasi tutte le funzioni che prendono una posizione e possono restituire null.

public static System.Collections.Generic.IEnumerable<T>  
    FunctionSequence(Func<int,T> f)  
{  
    int i = 0;
    while(true){
        value = f(i++);
        if(value != null) { 
            yield return value; 
        }
        else{
            break;
        }
    }
}  

foreach(GetValueType value in FunctionSequence<GetValueType>(GetValue)){
    DoSomethingWith(value);
}

Notare che ora non è necessario il codice piastra caldaia per qualsiasi funzione basta usare la funzione con FunctionSequence iterator e si itererà al primo elemento null, hai solo bisogno di scriverlo una volta, questo

Ulteriore generalizzazione della soluzione Iterator

Se non vuoi iniziare da 0, puoi sempre cambiare la firma della funzione in qualcosa del tipo:

public static System.Collections.Generic.IEnumerable<T>  
    FunctionSequence(Func<int,T> f, int start = 0)  
{  
    int i = start;
    ...

E potresti perfino cambiare il tuo controllo, se è stato restituito il suo oggetto nullo o un oggetto specifico!

public static System.Collections.Generic.IEnumerable<T>  
    FunctionSequence(Func<int,T> f, int start = 0, T terminator = null)  
{  
    int i = start;
    while(true){
        value = f(i++);
        if(value != terminator) { 
        ...

Questa penso sia la soluzione migliore per il tuo caso.

    
risposta data 31.05.2017 - 15:46
fonte
0

Vorrei andare con l'alternativa 3, perché non vedo un motivo per cui essere disponibile per i tipi di riferimento: soprattutto se non si utilizza un tipo di riferimento non è necessario controllare i valori nulli; nel caso in cui desideri qualche altra convalida come "questo enum ha il giusto valore?" puoi semplicemente usare quel controllo nel frattempo e verrebbe eseguito solo quando è appropriato (o potresti usare ref anziché out, anche se questo significa inizializzare la variabile quando lo dichiari)

    
risposta data 31.05.2017 - 14:21
fonte
0

Che ne dici di un'alternativa 4:

Racchiudere GetValue in un oggetto IEnumerable / IEnumerator. Sarai quindi in grado di utilizzare la normale sintassi foreach che consente lo scenario ideale. Se hai contenuto dinamico non hai bisogno di implementare una classe Collection completa, solo poche funzioni attorno a una dichiarazione di rendimento faranno il trucco.

Dovresti anche permetterti di usare alcuni trucchi magici di Linq anche con i tuoi dati.

    
risposta data 31.05.2017 - 15:42
fonte
0

Non sto facendo C # ma principalmente Swift. Nella maggior parte dei casi vorrei scrivere

for value in <some collection> { ... }

che è zucchero sintattico per

while let value = iterator.next() { }

(Il valore let = espressione valuta l'espressione, controlla se è nil, se nil esce, se non è nil memorizza un valore non facoltativo in value. next () in un iteratore restituisce un valore opzionale; nil se il valore l'iteratore è finito).

    
risposta data 01.06.2017 - 20:06
fonte

Leggi altre domande sui tag