foreach vs someList.ForEach () {}


167

컬렉션을 반복하는 방법은 여러 가지가 있습니다. 차이점이 있거나 왜 다른 방법으로 사용하는지 궁금합니다.

첫 번째 유형 :

List<string> someList = <some way to init>
foreach(string s in someList) {
   <process the string>
}

다른 방법 :

List<string> someList = <some way to init>
someList.ForEach(delegate(string s) {
    <process the string>
});

나는 내 머리 꼭대기에서 위에서 사용하는 익명 대리자 대신 지정할 수있는 재사용 가능한 대리자가 있다고 가정합니다 ...


답변:


134

둘 사이에는 중요하고 유용한 구별이 있습니다.

.ForEach는 for루프를 사용하여 컬렉션을 반복하므로 유효합니다 (편집 : .net 4.5 이전 -구현이 변경되어 둘 다 발생 함).

someList.ForEach(x => { if(x.RemoveMe) someList.Remove(x); }); 

반면 foreach열거자를 사용하므로 유효하지 않습니다.

foreach(var item in someList)
  if(item.RemoveMe) someList.Remove(item);

tl; dr :이 코드를 응용 프로그램에 복사하여 붙여 넣지 마십시오!

이 예제는 모범 사례가 아니며 ForEach()와 의 차이점을 보여주기위한 것 foreach입니다.

for루프 내의 목록에서 항목을 제거하면 부작용이있을 수 있습니다. 가장 일반적인 질문은이 질문에 대한 의견에 설명되어 있습니다.

일반적으로 목록에서 여러 항목을 제거하려는 경우 실제 제거에서 제거 할 항목을 결정하려는 경우가 있습니다. 코드를 컴팩트하게 유지하지는 않지만 항목을 놓치지 않도록 보장합니다.


35
그럼에도 불구하고 대신 someList.RemoveAll (x => x.RemoveMe)를 사용해야합니다.
Mark Cidade

2
Linq를 사용하면 모든 작업을 더 잘 수행 할 수 있습니다. 방금 foreach 내에서 컬렉션을 수정하는 예를 보여주었습니다.

2
RemoveAll ()은 List <T>의 메소드입니다.
Mark Cidade

3
당신은 아마 이것을 알고있을 것이지만, 사람들은 이런 식으로 아이템을 제거하는 것을 조심해야합니다. 항목 N을 제거하면 반복이 항목 (N + 1)을 건너 뛰고 자신의 for 루프에서 수행 한 것처럼 대리자에서 항목을 보거나 제거 할 기회를 얻지 못합니다.
El Zorko

9
for 루프 를 사용하여 목록을 거꾸로 반복하면 인덱스 이동 문제없이 항목을 제거 할 수 있습니다.
S. Tarık Çetin

78

여기에 이전 엔지니어 들이 작성한 모든 코드 list.ForEach( delegate(item) { foo;});대신 사용 하지 않는 코드가 VS2005 및 C # 2.0에 있습니다 foreach(item in list) {foo; };. 예를 들어 dataReader에서 행을 읽기위한 코드 블록.

나는 왜 그들이 왜 그렇게했는지 정확히 모른다.

단점 list.ForEach()은 다음 과 같습니다.

  • C # 2.0에서는 더 장황합니다. 그러나 C # 3부터는 " =>"구문을 사용하여 간결하게 표현할 수 있습니다.

  • 익숙하지 않습니다. 이 코드를 유지해야하는 사람들은 왜 그렇게했는지 궁금 할 것입니다. 작가를 영리하게 보이게 만드는 것 외에는 이유가 없다고 결정하는 데 잠시 시간이 걸렸습니다. })델리게이트 코드 블록의 끝에 " "가있어 읽기 가 어렵습니다.

  • Bill Wagner의 저서 "효과적인 C # : C # 개선을위한 50 가지 구체적인 방법"에서 foreach가 for 또는 while 루프와 같은 다른 루프보다 선호되는 이유에 대해 설명합니다. 주요 요점은 컴파일러가 구성하는 가장 좋은 방법을 결정하게하는 것입니다 루프. 향후 버전의 컴파일러가 더 빠른 기술을 사용할 수 있다면 코드를 변경하지 않고 foreach 및 rebuild를 사용하여 무료로 얻을 수 있습니다.

  • foreach(item in list)구조는 사용할 수 있습니다 break또는 continue당신이 반복하거나 루프를 종료해야하는 경우. 그러나 foreach 루프 내에서 목록을 변경할 수 없습니다.

나는 그것이 list.ForEach조금 더 빠르다는 것을보고 놀랐습니다 . 그러나 이것이 전체적으로 그것을 사용해야하는 정당한 이유가 아닐 수 있으므로 조기 최적화가 될 것입니다. 응용 프로그램에서 루프 제어가 아닌 데이터베이스 또는 웹 서비스를 사용하는 경우 거의 항상 시간이 지나는 위치에있게됩니다. 그리고 for루프에 대해서도 벤치마킹 했습니까? 는 list.ForEach내부적으로 그것을 사용하고 빠른 때문일 수 있습니다 for래퍼없이 루프가 더 빨리 될 것입니다.

나는 그 list.ForEach(delegate)버전이 어떤 방식 으로든 "더 기능적"이라고 동의하지 않는다 . 함수에 함수를 전달하지만 결과 또는 프로그램 구성에는 큰 차이가 없습니다.

나는 생각하지 않는다 foreach(item in list)"당신이 수행 할 방법을 정확하게 말한다"- for(int 1 = 0; i < count; i++)루프가 작업을 수행하는 foreach루프 잎 컴파일러 제어 최대의 선택.

새 프로젝트에서는 C # 3 " "연산자를 사용하여보다 우아하고 간결하게 작업을 수행 할 수있는 경우 foreach(item in list)일반적인 사용법과 가독성을 유지하기 위해 대부분의 루프에 사용하고 list.Foreach()짧은 블록에만 사용 하는 것이 =>좋습니다. 그런 경우에는보다 구체적인 LINQ 확장 방법이 이미있을 수 있습니다 ForEach(). 경우를 참조하십시오 Where(), Select(), Any(), All(), Max()다른 많은 LINQ 방법 또는 1이 이미 루프에서하고 싶은 일을하지 않습니다.


호기심을 위해서 ... Microsoft 구현을보십시오 ... referencesource.microsoft.com/#mscorlib/system/collections/…
Alexandre

17

재미를 위해 List를 리플렉터에 팝하고 결과 C #입니다.

public void ForEach(Action<T> action)
{
    if (action == null)
    {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
    }
    for (int i = 0; i < this._size; i++)
    {
        action(this._items[i]);
    }
}

마찬가지로 foreach에서 사용하는 열거 자의 MoveNext는 다음과 같습니다.

public bool MoveNext()
{
    if (this.version != this.list._version)
    {
        ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion);
    }
    if (this.index < this.list._size)
    {
        this.current = this.list._items[this.index];
        this.index++;
        return true;
    }
    this.index = this.list._size + 1;
    this.current = default(T);
    return false;
}

List.ForEach는 처리가 훨씬 적은 MoveNext보다 훨씬 잘 정리되어보다 효율적인 JIT가 될 것입니다.

또한 foreach ()는 무엇이든 새로운 열거자를 할당합니다. GC의 당신의 친구이지만, 반복적으로 같은 foreach 문을 수행하는 경우,이 같은 대리자를 재사용 반대로, 더 일회용 객체를 만들 것입니다 - 하지만 -이 정말 프린지 경우입니다. 일반적인 사용법에서는 차이가 거의 없거나 전혀 없습니다.


1
foreach에 의해 생성 된 코드가 컴파일러 버전간에 동일하다는 보장은 없습니다. 생성 된 코드는 향후 버전으로 개선 될 수 있습니다.
Anthony

1
.NET Core 3.1 현재 ForEach는 여전히 빠릅니다.
jackmott

13

나는 그것들을 다르게 만드는 두 가지 모호한 것을 알고 있습니다. 가요

첫째, 목록의 각 항목에 대한 대리인을 만드는 데있어 고전적인 버그가 있습니다. foreach 키워드를 사용하면 모든 대리인이 목록의 마지막 항목을 참조 할 수 있습니다.

    // A list of actions to execute later
    List<Action> actions = new List<Action>();

    // Numbers 0 to 9
    List<int> numbers = Enumerable.Range(0, 10).ToList();

    // Store an action that prints each number (WRONG!)
    foreach (int number in numbers)
        actions.Add(() => Console.WriteLine(number));

    // Run the actions, we actually print 10 copies of "9"
    foreach (Action action in actions)
        action();

    // So try again
    actions.Clear();

    // Store an action that prints each number (RIGHT!)
    numbers.ForEach(number =>
        actions.Add(() => Console.WriteLine(number)));

    // Run the actions
    foreach (Action action in actions)
        action();

List.ForEach 메서드에는이 문제가 없습니다. 반복의 현재 항목은 값으로 외부 람다에 대한 인수로 전달 된 다음 내부 람다는 해당 인수를 자체 클로저로 올바르게 캡처합니다. 문제 해결됨.

(슬프게도 ForEach는 확장 방법이 아니라 List의 멤버라고 생각하지만 직접 정의하기가 쉽지만 열거 가능한 유형 의이 시설을 갖습니다.)

둘째, ForEach 방법은 한계가 있습니다. yield return을 사용하여 IEnumerable을 구현하는 경우 람다 내에서 yield return을 수행 할 수 없습니다. 따라서이 방법으로는 반환 작업을 수행하기 위해 컬렉션의 항목을 반복하는 것이 불가능합니다. foreach 키워드를 사용해야하며 루프 내에서 현재 루프 값의 사본을 수동으로 만들어 클로저 문제를 해결해야합니다.

여기 더


5
언급 한 문제 foreach는 C # 5에서 "고정"되었습니다. stackoverflow.com/questions/8898925/…
StriplingWarrior

13

나는 생각 someList.ForEach()정상은 반면에 통화가 쉽게 병렬화 될 수있는 foreach병렬 실행 쉬운 것이 아니다. 다른 코어에서 여러 개의 다른 델리게이트를 쉽게 실행할 수 있습니다 foreach.
내 2 센트 만


2
그는 런타임 엔진이 자동으로 병렬화 할 수 있다고 생각했습니다. 그렇지 않으면 foreach와 .ForEach를 각 작업 대리자의 풀에서 스레드를 사용하여 손으로 병렬 처리 할 수 ​​있습니다
Isak Savo

@Isak이지만 잘못된 가정 일 것입니다. 익명 메소드가 로컬 또는 멤버를 게양하면 런타임이 쉽게 병렬화 될 수 없습니다.
Rune FS

6

그들이 말하는 것처럼, 악마는 세부 사항에 있습니다 ...

컬렉션 열거의 두 가지 방법의 가장 큰 차이점 foreach은 상태 를 전달하는 반면 ForEach(x => { })그렇지는 않습니다.

그러나 결정에 영향을 줄 수있는 몇 가지 사항이 있고 두 경우 모두를 코딩 할 때주의해야 할 사항이 있기 때문에 좀 더 깊이 파고 들겠습니다.

사용할 수 있습니다 List<T>행동을 관찰하기 위해 우리의 작은 실험에서. 이 실험에서는 .NET 4.7.2를 사용하고 있습니다.

var names = new List<string>
{
    "Henry",
    "Shirley",
    "Ann",
    "Peter",
    "Nancy"
};

foreach먼저 이것을 반복합니다 :

foreach (var name in names)
{
    Console.WriteLine(name);
}

이를 다음과 같이 확장 할 수 있습니다.

using (var enumerator = names.GetEnumerator())
{

}

열거자를 손에 넣은 다음 덮개를 살펴보십시오.

public List<T>.Enumerator GetEnumerator()
{
  return new List<T>.Enumerator(this);
}
    internal Enumerator(List<T> list)
{
  this.list = list;
  this.index = 0;
  this.version = list._version;
  this.current = default (T);
}

public bool MoveNext()
{
  List<T> list = this.list;
  if (this.version != list._version || (uint) this.index >= (uint) list._size)
    return this.MoveNextRare();
  this.current = list._items[this.index];
  ++this.index;
  return true;
}

object IEnumerator.Current
{
  {
    if (this.index == 0 || this.index == this.list._size + 1)
      ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumOpCantHappen);
    return (object) this.Current;
  }
}

두 가지가 즉시 명백해진다 :

  1. 기본 컬렉션에 대한 친밀한 지식을 갖춘 상태 저장 객체가 반환됩니다.
  2. 컬렉션의 복사본은 얕은 복사본입니다.

이것은 물론 스레드 안전이 아닙니다. 위에서 지적했듯이 반복하는 동안 컬렉션을 변경하는 것은 나쁜 모조입니다.

그러나 반복하는 동안 컬렉션을 모으는 외부의 수단을 통해 반복하는 동안 컬렉션이 무효화되는 문제는 어떻습니까? 모범 사례에서는 작업 및 반복 중에 컬렉션의 버전을 관리하고 기본 컬렉션이 변경되는시기를 감지하기 위해 버전을 확인하는 것이 좋습니다.

여기는 정말 어두운 곳입니다. Microsoft 설명서에 따르면 :

요소 추가, 수정 또는 삭제와 같이 컬렉션이 변경되면 열거 자의 동작이 정의되지 않습니다.

그게 무슨 뜻이야? 예를 들어, List<T>예외 처리를 구현한다고해서 구현하는 모든 컬렉션이 IList<T>동일하게 수행되는 것은 아닙니다 . 이는 Liskov 대체 원칙을 명백히 위반 한 것으로 보입니다.

수퍼 클래스의 객체는 응용 프로그램을 중단하지 않고 서브 클래스의 객체로 대체 가능해야합니다.

또 다른 문제는 열거자가 구현해야한다는 것 IDisposable입니다. 즉, 호출자가 잘못했을 때뿐만 아니라 작성자가 Dispose패턴을 올바르게 구현하지 않은 경우 잠재적 인 메모리 누수의 또 다른 원인이 됩니다.

마지막으로, 우리는 평생 문제가 있습니다 ... 반복자가 유효하지만 기본 컬렉션이 사라지면 어떻게됩니까? 우리는 지금의 스냅 샷 무엇인지는 당신이 수집하고 반복자의 수명을 분리 할 때 ..., 당신은 문제가 요구된다.

이제 살펴 보자 ForEach(x => { }):

names.ForEach(name =>
{

});

이것은 다음과 같이 확장됩니다.

public void ForEach(Action<T> action)
{
  if (action == null)
    ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
  int version = this._version;
  for (int index = 0; index < this._size && (version == this._version || !BinaryCompatibility.TargetsAtLeast_Desktop_V4_5); ++index)
    action(this._items[index]);
  if (version == this._version || !BinaryCompatibility.TargetsAtLeast_Desktop_V4_5)
    return;
  ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion);
}

중요한 사항은 다음과 같습니다.

for (int index = 0; index < this._size && ... ; ++index) action(this._items[index]);

이 코드는 열거자를 할당하지 않으며 ( Dispose)에 반복 하지 않고 일시 중지 하지 않습니다 .

이것은 또한 기본 콜렉션의 단순 사본을 수행하지만 콜렉션은 이제 스냅 샷입니다. 작성자가 컬렉션 변경 또는 'stale'에 대한 검사를 올바르게 구현하지 않은 경우 스냅 샷은 여전히 ​​유효합니다.

이것은 어떤 식 으로든 평생 문제의 문제로부터 당신을 보호하지 않습니다 ... 기본 컬렉션이 사라지면 이제는 무엇을 가리키는 얕은 사본이 있지만 적어도 Dispose문제 는 없습니다. 고아 반복자 다루기 ...

그렇습니다. 반복자는 ... 때때로 상태를 갖는 것이 유리하다고 말했습니다. 데이터베이스 커서와 비슷한 것을 유지하고 싶다고 가정합시다. 아마도 여러 foreach스타일 Iterator<T>이 갈 길입니다. 나는 평생 문제가 너무 많기 때문에 개인적 으로이 스타일의 디자인을 싫어하고, 당신이 믿고있는 컬렉션의 저자의 은혜에 의존합니다 (문자 그대로 모든 것을 처음부터 작성하지 않는 한).

항상 세 번째 옵션이 있습니다 ...

for (var i = 0; i < names.Count; i++)
{
    Console.WriteLine(names[i]);
}

섹시하지는 않지만 치아가 있습니다 ( 톰 크루즈 와 영화 The Firm 에게 사과 )

그것은 당신의 선택이지만, 지금 당신은 알고 있으며 정보를 얻을 수 있습니다.


5

익명 대리인의 이름을 지정할 수 있습니다 :-)

그리고 당신은 두 번째로 쓸 수 있습니다 :

someList.ForEach(s => s.ToUpper())

어떤 것을 선호하고 많은 타이핑을 저장합니다.

Joachim이 말했듯이 병렬 처리는 두 번째 양식에 적용하기가 더 쉽습니다.


2

장면 뒤에서 익명 대리자가 실제 메서드로 바뀌므로 컴파일러가 함수를 인라인하도록 선택하지 않은 경우 두 번째 선택으로 오버 헤드가 발생할 수 있습니다. 또한 익명 대리자 예제 본문에서 참조하는 모든 로컬 변수는 새로운 메서드로 컴파일된다는 사실을 숨기는 컴파일러 트릭으로 인해 본질적으로 변경됩니다. C #이이 마법을 수행하는 방법에 대한 자세한 정보는 다음과 같습니다.

http://blogs.msdn.com/oldnewthing/archive/2006/08/04/688527.aspx


2

ForEach 함수는 일반 클래스 List의 멤버입니다.

내부 코드를 재현하기 위해 다음 확장을 만들었습니다.

public static class MyExtension<T>
    {
        public static void MyForEach(this IEnumerable<T> collection, Action<T> action)
        {
            foreach (T item in collection)
                action.Invoke(item);
        }
    }

결국 우리는 정상적인 foreach (또는 원하는 경우 루프)를 사용하고 있습니다.

반면에 델리게이트 함수를 사용하는 것은 함수를 정의하는 또 다른 방법입니다.

delegate(string s) {
    <process the string>
}

다음과 같습니다.

private static void myFunction(string s, <other variables...>)
{
   <process the string>
}

또는 labda 표현식 사용 :

(s) => <process the string>

2

전체 ForEach 범위 (위임 함수)는 한 줄의 코드 (함수 호출)로 취급되며 중단 점을 설정하거나 코드에 들어갈 수 없습니다. 처리되지 않은 예외가 발생하면 전체 블록이 표시됩니다.


2

List.ForEach ()는 더 기능적인 것으로 간주됩니다.

List.ForEach()당신이 원하는 것을 말합니다. foreach(item in list)또한 당신이 원하는 것을 정확하게 말합니다. 이것은 미래 List.ForEach방법 부분 의 구현을 자유롭게 바꿀 수 있습니다. 예를 들어, 가상의 미래 버전의 .Net은 항상 List.ForEach병렬로 실행될 수 있습니다 .이 시점에서 모든 사람이 일반적으로 유휴 상태 인 많은 CPU 코어가 있다고 가정합니다.

반면에 foreach (item in list)루프를 조금 더 제어 할 수 있습니다. 예를 들어, 항목이 어떤 종류의 순차적 순서로 반복된다는 것을 알고 있으며 항목이 특정 조건을 충족하면 중간에서 쉽게 끊을 수 있습니다.


이 문제에 대한 최근의 언급은 다음에서 확인할 수 있습니다.

https://stackoverflow.com/a/529197/3043


1

두 번째로 보여준 방법은 확장 메서드를 사용하여 목록의 각 요소에 대해 대리자 메서드를 실행하는 것입니다.

이렇게하면 다른 대리자 (= method) 호출이 있습니다.

또한 for 루프 를 사용하여 목록을 반복 할 수도 있습니다.


1

주의해야 할 사항은 Generic.ForEach 메서드를 종료하는 방법 입니다. 이 설명을 참조하십시오 . 링크는이 방법이 가장 빠르다고 말합니다. 왜 그런지 모르겠습니다-일단 컴파일되면 동등하다고 생각할 것입니다 ...


0

방법이 있습니다. 내 앱에서 해냈습니다.

List<string> someList = <some way to init>
someList.ForEach(s => <your actions>);

foreach에서 아이템으로 사용할 수 있습니다

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.