2014-11-04 4 views
22

Das muss etwas wirklich einfaches sein. Aber ich werde es trotzdem fragen, weil ich denke, dass andere auch damit kämpfen werden. Warum wird die folgende einfache LINQ-Abfrage nicht immer mit dem neuen Variablenwert ausgeführt, statt immer den ersten zu verwenden?LINQs verzögerte Ausführung, aber wie?

static void Main(string[] args) 
{ 
    Console.WriteLine("Enter something:"); 
    string input = Console.ReadLine();  // for example ABC123 
    var digits = input.Where(Char.IsDigit); // 123 
    while (digits.Any()) 
    { 
     Console.WriteLine("Enter a string which doesn't contain digits"); 
     input = Console.ReadLine();   // for example ABC 
    } 
    Console.WriteLine("Bye"); 
    Console.ReadLine(); 
} 

In der kommentierten Proben wird er die Schleife eingeben, da der Eingang ABC123 Ziffern enthält. Aber es wird es nie verlassen, auch wenn Sie so etwas wie ABC eingeben, da digits immer noch 123 ist.

Warum also die LINQ-Abfrage nicht den neuen input -Wert auswerten, sondern immer den ersten?

weiß ich, ich es mit dieser zusätzlichen Zeile beheben könnte:

while (digits.Any()) 
{ 
    Console.WriteLine("Enter a string which doesn't contain digits"); 
    input = Console.ReadLine();   
    digits = input.Where(Char.IsDigit); // now it works as expected 
} 

oder - eleganter - durch die Abfrage direkt in der Schleife:

while (input.Any(Char.IsDigit)) 
{ 
    // ... 
} 
+5

Wenn Sie eine Variable als Parameter an eine Funktion übergeben, wird sie als Wert übergeben. –

+1

Solch ein einfaches Stück Code mit solch komplexen Nebenwirkungen. –

+2

Heilige Kuh, du hast eine Antwort von The Actual Raymond Chen ™ erhalten! –

Antwort

40

Der Unterschied ist, dass man den Wert der input Variable sind zu ändern, anstatt die Inhalt des Objekts, dass die Variable ... so digits noch auf die ursprüngliche Sammlung bezieht sich bezieht.

Vergleichen Sie das mit diesem Code:

List<char> input = new List<char>(Console.ReadLine()); 
var digits = input.Where(Char.IsDigit); // 123 
while (digits.Any()) 
{ 
    Console.WriteLine("Enter a string which doesn't contain digits"); 
    input.Clear(); 
    input.AddRange(Console.ReadLine()); 
} 

Diesmal sind modifizieren wir den Inhalt der Sammlung, dass input bezieht sich auf - und als digits über diese Sammlung effektiv eine Ansicht ist, erhalten wir Siehe die Änderung.

+0

Bin ich richtig - das ist im Frage-Beispiel wegen Unveränderlichkeit von Strings passiert? – fex

+1

@fex: Die Unveränderlichkeit der Strings war nicht der Grund für dieses Problem, aber es war der Grund für meine Verwirrung. Wenn die Zeichenfolge eine Auflistung wie "Liste " wäre, würde ich sie direkt ändern und der Variablen keine neue Liste zuweisen. –

+1

@fex: Nicht direkt. Wenn Zeichenketten änderbar wären, würde das Ändern des Wertes von "Eingabe" * noch * keine Auswirkungen auf "Ziffern" haben, sondern der Inhalt, auf den sich "Eingabe" bezieht, könnte stattdessen geändert worden sein. –

10

Sie einen neuen Wert zuweisen zu input, aber die digits Sequenz wird immer noch von dem Anfangswert von input abgeleitet. Mit anderen Worten, wenn Sie digits = input.Where(Char.IsDigit), tun, erfasst es den aktuellen Wert der input Variable, nicht die Variable selbst. Das Zuweisen eines neuen Werts zu input hat keine Auswirkungen auf digits.

4

Die Zahl Enumerable bezieht sich auf eine Kopie der Zeichenfolge, die input enthielt, als Sie den Enumerable erstellten. Es enthält keinen Verweis auf die Variable input, und das Ändern des in input gespeicherten Werts führt nicht dazu, dass Materialisierungen des Enumerables den neuen Wert verwenden.

Denken Sie daran, dass Where eine statische Erweiterungsmethode ist und das Objekt, für das Sie es aufrufen, als Parameter akzeptiert.

6

diese Zeile:

input.Where(Char.IsDigit) 

entspricht:

Enumerable.Where(input, Char.IsDigit) 

Somit wird der Wert von input als Quelle der .Where Abfrage übergeben wird, keine Referenz zu input.

Der erste Fix, den Sie vorgeschlagen haben, funktioniert, weil er den neu zugewiesenen Wert input in der vorherigen Zeile verwendet.

+0

Ja. Man kann sagen, dass "input" ein ByValue-Parameter ist, kein ByRef-Parameter (es sagt nicht "ref" oder "out"). –

+0

@JeppeStigNielsen: Das stimmt eigentlich nicht. Zeichenfolgen sind Referenztypen (deshalb können Sie eine Null-Zeichenfolge haben). Das bedeutet, dass die Variable "input" tatsächlich einen Verweis auf den String enthält. Wenn 'string' veränderbar wäre, könnten Sie eine Zeichenfolge als normalen Parameter an eine Funktion übergeben, und das Ändern der Zeichenfolge innerhalb würde auch die ursprüngliche Zeichenfolge ändern. Da Sie jedoch keine Zeichenfolgen ändern können, hat es in einigen Szenarien ähnliche Auswirkungen wie die Übergabe von Parametern als ByValue. –

+0

@tomp Ich weiß 'string' ist ein Referenztyp. Das war nicht das, worum ich mich kümmerte. Ich ging davon aus, ob der Parameter ein ByRef-Parameter (entweder 'Ref-String' oder 'Out-String') war oder nicht. Also stimmen wir zu. Es ist bedauerlich, dass die zwei unterschiedlichen Begriffe "Referenztyp" (zum Beispiel "Klasse" (usw.), nicht "struct") und "by ref Parameter" (entweder "ref" oder "out") so ähnliche Namen haben. Es führt zu vielen Missverständnissen. Ich war mir dessen bewusst und versuchte, meinen Wortlaut präzise zu formulieren, aber trotzdem wurde ich falsch verstanden ... –

2

Ich antworte nur, um eine Genauigkeit zu den anderen guten Antworten hinzuzufügen, über die verzögerte Ausführung.

Auch wenn die LINQ-Abfrage noch nicht ausgewertet wurde (unter Verwendung von .Any()), bezieht sich die Abfrage intern immer auf den ursprünglichen Inhalt der Variablen . Auch wenn die LINQ-Abfrage nach etwas Neues ausgewertet hat auf die Variable betroffen, wird der anfängliche Inhalt nicht ändern und die verzögerte Ausführung wird den anfänglichen Inhalt der Abfrage ist immer bezogen hat:

var input = "ABC123"; 
var digits = input.Where(Char.IsDigit); 
input = "NO DIGIT"; 
var result = digits.ToList(); // 3 items 
+1

Seien Sie nur gewarnt, dass "anfänglicher Inhalt" auf eine veränderbare Struktur verweisen kann. z.B. 'input = new Liste {1}; var even = Eingabe.where (x => x% 2 == 0); input.Add (2); var result = even.ToList(); ' – NPSF3000

+2

@ NPSF3000: wenn es eine veränderbare Sammlung wäre, hätte ich diese Frage wegen des Fehlens eines Problems nicht gestellt ;-) –

4

Das ist fast ein Kommentar, enthält aber strukturierten Code, also gebe ich ihn als Antwort ein.

wird die folgende leichte Modifikation des Codes arbeiten:

Console.WriteLine("Enter something:"); 
    string input = Console.ReadLine();  // for example ABC123 
    Func<bool> anyDigits =() => input.Any(Char.IsDigit); // will capture 'input' as a field 
    while (anyDigits()) 
    { 
    Console.WriteLine("Enter a string which doesn't contain digits"); 
    input = Console.ReadLine();   // for example ABC 
    } 
    Console.WriteLine("Bye"); 
    Console.ReadLine(); 

Hier input (Verschluss) erfasst wird von den Delegierten des Typs Func<bool>.

+0

ReSharper winselt," Zugang zu modifiziertem Verschluss " für die erste Verwendung von "Eingabe". Ich schlage vor, dass Sie zu 'Func ' wechseln und mit 'while (anyDigits (input))' aufrufen (was die Lesbarkeit verbessert). – onedaywhen

+0

@onedaywhen Ja! Dies sollte nicht der beste Weg sein, den Code zu schreiben. Es sollte nur eine "minimale" Änderung des ursprünglichen Codes (von der Frage) sein, die tatsächlich funktioniert hat. Ich empfehle oder empfehle Leuten nicht, wie oben beschrieben zu programmieren. Der Grund, warum ReSharper Sie warnt, ist, dass die Schließsemantik für den Leser des Codes verwirrend sein kann. Um die gewünschte Funktionalität zu erhalten, benötigen wir keinen Zugriff auf die modifizierte Schließung (und der Fragesteller weiß bessere Möglichkeiten, um die Dinge bereits in der Frage funktionieren zu lassen). –