foreach에서 C #이 변수를 재사용하는 이유가 있습니까?


1684

C #에서 람다 식 또는 익명 메서드를 사용하는 경우 수정 된 클로저 함정 에 대한 액세스에 주의해야합니다 . 예를 들면 다음과 같습니다.

foreach (var s in strings)
{
   query = query.Where(i => i.Prop == s); // access to modified closure
   ...
}

수정 된 클로저로 인해 위의 코드는 Where쿼리 의 모든 절이의 최종 값을 기반으로하도록합니다 s.

here 설명 된 것처럼 위의 루프 s에서 선언 된 변수 foreach가 컴파일러에서 다음과 같이 변환 되기 때문에 발생 합니다.

string s;
while (enumerator.MoveNext())
{
   s = enumerator.Current;
   ...
}

이 대신에 :

while (enumerator.MoveNext())
{
   string s;
   s = enumerator.Current;
   ...
}

here 에서 지적했듯이 루프 외부에서 변수를 선언하면 성능 이점이 없으며 일반적인 상황에서 루프 범위 밖에서 변수를 사용하려는 경우이를 생각할 수있는 유일한 이유는 다음과 같습니다.

string s;
while (enumerator.MoveNext())
{
   s = enumerator.Current;
   ...
}
var finalString = s;

그러나 foreach루프에 정의 된 변수는 루프 외부에서 사용할 수 없습니다.

foreach(string s in strings)
{
}
var finalString = s; // won't work: you're outside the scope.

따라서 컴파일러는 변수를 선언하고 디버깅하기 어려운 오류가 발생하기 쉬운 방식으로 선언하지만 인식 가능한 이점은 없습니다.

foreach내부 범위 변수로 컴파일 된 경우 불가능한 방식 으로 루프로 할 수있는 작업이 있습니까? 아니면 익명 메소드와 람다 식을 사용할 수 있거나 공통적이기 전에 만들어진 임의의 선택입니까? 그때 이후로 수정되지 않았습니까?


4
무슨 일이야 String s; foreach (s in strings) { ... }?
Brad Christie

5
@BradChristie OP는 실제로 이야기하고 foreach있지 않지만 OP에 의해 표시된 것과 유사한 코드를 생성하는 람다 식에 대해 ...
Yahia

22
@ 브래드 크리스티 : 컴파일됩니까? ( 오류 : 유형과 식별자는 모두 foreach 명세서에 필요합니다)
Austin Salonen

32
@JakobBotschNielsen : 람다의 폐쇄 된 외부 지역입니다. 왜 스택에 있다고 가정합니까? 수명이 스택 프레임보다 길다 !
Eric Lippert

3
@EricLippert : 혼란 스러워요. 람다는 foreach 변수 ( 루프 외부 에서 선언 된)에 대한 참조를 캡처 하므로 최종 값과 비교 한다는 것을 이해합니다 . 내가 얻는. 내가 이해하지 못하는 것은 루프 내부 에서 변수를 선언하면 어떻게 차이가 나는지입니다. 컴파일러 작성자의 관점에서 나는 선언이 루프 내부 또는 외부에 있는지 여부에 관계없이 스택에 하나의 문자열 참조 (var 's') 만 할당합니다. 나는 매번 반복 할 때마다 새로운 참조를 스택에 푸시하고 싶지 않습니다!
Anthony

답변:


1407

컴파일러는 변수를 선언하고 디버깅하기 어려운 오류가 발생하기 쉬운 방식으로 선언하지만 인식 가능한 이점은 없습니다.

당신의 비판은 전적으로 정당합니다.

이 문제에 대해 자세히 설명합니다.

유해한 것으로 간주되는 루프 변수를 닫으면

foreach 루프로 내부 범위 변수로 컴파일 된 경우 불가능한 방법이 있습니까? 아니면 익명의 메서드와 람다 식을 사용할 수 있거나 일반적으로 사용되기 전에 만들어진 임의의 선택입니까? 그 이후로 수정되지 않은 것은 무엇입니까?

후자의. C # 1.0 사양에서는 실제로 루프 변수가 루프 본문 내부 또는 외부에 있는지 여부를 밝히지 않았습니다. 눈에 띄는 차이가 없었습니다. C # 2.0에서 클로저 시맨틱이 도입되었을 때 "for"루프와 일치하도록 루프 변수를 루프 외부에 두도록 선택했습니다.

모든 사람들이 그 결정을 후회한다고 말하는 것이 공정하다고 생각합니다. 이것은 C #에서 최악의 "gotchas"중 하나이며 이를 수정하기 위해 주요 변경 사항을 적용 할 것입니다. C # 5에서 foreach 루프 변수는 논리적으로 루프 본문 안에 있으므로 클로저는 매번 새로운 사본을 얻습니다.

for루프가 변경되지 않으며, 변경이되지 않습니다 C #을 이전 버전으로 "다시는 포팅". 따라서이 관용구를 사용할 때는 계속주의해야합니다.


177
실제로 C # 3과 C # 4에서이 변경 사항을 적용하지 않았습니다. C # 3을 설계 할 때 람다 (및 쿼리)가 너무 많기 때문에 (C # 2에 이미 존재했던) 문제가 악화 될 것이라는 것을 깨달았습니다. LINQ 덕분에 foreach 루프에서 이해력이 뛰어납니다. C # 3에서 문제를 해결하기보다는 너무 늦게 문제를 해결하기 위해 문제가 충분히 나빠지기를 기다린 것을 후회합니다.
Eric Lippert

75
그리고 지금 우리 foreach는 '안전하다' 는 것을 기억해야 하지만 for그렇지 않습니다.
leppie

22
@michielvoo : 이전 버전과 호환되지 않는다는 의미에서 변화가 일어나고 있습니다. 이전 컴파일러를 사용할 때 새 코드가 올바르게 실행되지 않습니다.
leppie

41
@Benjol : 아니요, 우리가 기꺼이 받아들이는 이유입니다. Jon Skeet은 중요한 변경 변경 시나리오를 지적했습니다. 누군가 C # 5로 코드를 작성하고 테스트 한 다음 여전히 C # 4를 사용하는 사람들과 공유하는 것이 중요합니다. 그러한 시나리오에 영향을받는 사람들의 수가 적기를 바랍니다.
Eric Lippert

29
한편, ReSharper는이를 항상 파악하여 "수정 된 폐쇄에 대한 액세스"로보고합니다. 그런 다음 Alt + Enter를 누르면 자동으로 코드가 수정됩니다. jetbrains.com/resharper
Mike Chamberlain

191

에릭 리퍼 (Eric Lippert)는 자신의 블로그 포스트에서 유해한 것으로 간주되는 루프 변수에 대한 Closing 과 그 속편을 철저히 다루었습니다 .

나에게 가장 설득력있는 주장은 각 반복에서 새로운 변수를 갖는 것이 for(;;)스타일 루프 와 일치하지 않는다는 것 입니다. int i반복 할 때마다 새로운 것을 기대하겠습니까 for (int i = 0; i < 10; i++)?

이 동작에서 가장 일반적인 문제는 반복 변수에 대한 클로저를 만드는 것이며 쉬운 해결 방법이 있습니다.

foreach (var s in strings)
{
    var s_for_closure = s;
    query = query.Where(i => i.Prop == s_for_closure); // access to modified closure

이 문제에 대한 내 블로그 게시물 : C #에서 foreach 변수에 대한 폐쇄 .


18
궁극적으로 사람들 이 이것을 작성할 때 실제로 원하는 것은 여러 변수를 갖지 않고 을 닫는 것 입니다. 그리고 일반적인 경우에 사용 가능한 구문을 생각하기는 어렵습니다.
Random832

1
예, 값으로 닫을 수는 없지만 포함하기 위해 답변을 편집 한 매우 쉬운 해결 방법이 있습니다.
Krizz

6
C #에서 너무 나쁜 클로저는 참조를 닫습니다. 기본적으로 값을 닫으면 대신 변수를 닫는 것을 쉽게 지정할 수 있습니다 ref.
Sean U

2
@Krizz, 강제 일관성이 일관성이없는 것보다 더 해로운 경우입니다. 사람들이 기대하는대로 "정상적으로 작동"해야하며, for 루프를 사용하는 대신 foreach를 사용할 때 사람들이 수정 된 클로저 문제 (예 : 나 자신과 같은)에 접근하기 전에 문제를 겪은 사람들의 수를 감안할 때 사람들은 분명히 다른 것을 기대해야합니다. .
Andy

2
Random832가 C 번호에 대해 있지만 일반적인 LISP에서 모르는 @ 거기에 대한 구문이며, 가변 변수와 폐쇄와 모든 언어가 (아니, 것이 수치 필수 ) 너무 그것이있다. 우리는 변화하는 장소에 대한 언급이나 주어진 순간 (폐쇄의 생성)에 가지고있는 가치에 가깝습니다. 이것은 Python과 Scheme의 유사한 것들에 대해 설명합니다 ( cutrefs / vars 및 부분적으로 평가 된 클로저에 평가 된cute을 유지하기 위한 것 ).
Will Ness

103

이것에 물린 후, 나는 가장 가까운 범위에 로컬로 정의 된 변수를 포함시키는 습관을 가지고 있습니다. 귀하의 예에서 :

foreach (var s in strings)
    query = query.Where(i => i.Prop == s); // access to modified closure

나는한다:

foreach (var s in strings)
{
    string search = s;
    query = query.Where(i => i.Prop == search); // New definition ensures unique per iteration.
}        

일단 그 습관을 가지면, 실제로는 외부 스코프에 바인딩하려는 아주 드문 경우에 이를 피할 수 있습니다 . 솔직히 말해서, 나는 그렇게 한 적이 없다고 생각합니다.


24
이것이 일반적인 해결 방법입니다. 기여해 주셔서 감사합니다. Resharper는이 패턴을 인식하고주의를 기울일만큼 똑똑합니다. 에릭 리퍼 (Eric Lippert)의 말에 따르면,이 패턴에 한참 가까워지지는 않았지만 "우리가 얻는 가장 일반적인 잘못된 버그 리포트 하나" 를 피하는 방법 보다 더 많은 이유 를 알고 싶었 습니다 .
StriplingWarrior

62

C # 5.0에서는이 문제가 해결되었으며 루프 변수를 닫고 예상 한 결과를 얻을 수 있습니다.

언어 사양은 다음과 같습니다.

8.8.4 foreach 문

(...)

형식의 foreach 문

foreach (V v in x) embedded-statement

그런 다음 다음으로 확장됩니다.

{
  E e = ((C)(x)).GetEnumerator();
  try {
      while (e.MoveNext()) {
          V v = (V)(T)e.Current;
          embedded-statement
      }
  }
  finally {
       // Dispose e
  }
}

(...)

v내부 루프 의 배치 는 임베디드 명령문에서 발생하는 익명 함수에 의해 캡처되는 방법에 중요합니다. 예를 들면 다음과 같습니다.

int[] values = { 7, 9, 13 };
Action f = null;
foreach (var value in values)
{
    if (f == null) f = () => Console.WriteLine("First value: " + value);
}
f();

vwhile 루프 외부에서 선언 된 경우 모든 반복간에 공유되며 for 루프 이후의 값은 최종 값 이됩니다. 13이 값 은 호출 f이 인쇄 하는 것입니다. 대신, 각 반복마다 고유 한 변수 가 있으므로 첫 번째 반복에서 v캡처 한 변수 f는 계속 값을 유지하여 7인쇄됩니다. ( 참고 : 이전 버전의 C # v은 while 루프 외부에서 선언되었습니다 . )


1
이 초기 버전의 C #이 while 루프 내에서 v를 선언 한 이유는 무엇입니까? msdn.microsoft.com/en-GB/library/aa664754.aspx
colinfang

4
@colinfang Eric의 대답을 반드시 읽으십시오 : C # 1.0 사양 ( 링크에서 VS 2003, 즉 C # 1.2에 대해 이야기하고 있음 )은 실제로 루프 변수가 루프 본문 내부 또는 외부에 있는지 여부를 말하지 않았습니다. 눈에 띄는 차이가 없습니다. . C # 2.0에서 클로저 시맨틱이 도입되었을 때 "for"루프와 일치하도록 루프 변수를 루프 외부에 두도록 선택했습니다.
Paolo Moretti

1
링크의 예제가 그 당시 결정적인 사양이 아니라고 말하고 있습니까?
colinfang

4
@colinfang 그들은 명확한 사양이었습니다. 문제는 우리가 나중에 (C # 2.0에서) 소개 된 기능 (즉, 함수 클로저)에 대해 이야기하고 있다는 것입니다. C # 2.0이 나타 났을 때 루프 변수를 루프 외부에두기로 결정했습니다. 그리고 그들이 C # 5.0으로 다시 마음을 바꾼 것보다 :)
Paolo Moretti
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.