대 foreach 대 LINQ


86

Visual Studio에서 코드를 작성할 때 ReSharper (God bless!)는 종종 구식 학교 for 루프를보다 간결한 foreach 형식으로 변경하도록 제안합니다.

그리고 종종이 변경 사항을 수락하면 ReSharper는 한발 더 나아가 반짝이 LINQ 형식으로 다시 변경하도록 제안합니다.

그래서, 나는 이 개선에 몇 가지 진정한 장점이 있습니까? 아주 간단한 코드 실행에서는 속도 향상을 볼 수 없지만 코드가 점점 더 읽기 어려워지는 것을 볼 수 있습니다 ... 그래서 궁금합니다 : 그만한 가치가 있습니까?


2
참고 사항-LINQ 구문은 SQL 구문에 익숙하면 실제로 읽을 수 있습니다. LINQ에는 두 가지 형식 (SQL과 같은 람다 식 및 연결 방법)이있어 배우기가 더 쉽습니다. 읽을 수없는 것처럼 보이게하는 ReSharper의 제안 일 수 있습니다.
Shauna

3
일반적으로 알려진 반복 길이 또는 반복 횟수와 관련된 유사한 경우를 다루지 않는 한 일반적으로 foreach를 사용합니다. LINQ 화에 관해서는, 일반적으로 ReSharper가 foreach로 무엇을 만드는지 알 수 있으며 결과 LINQ 문이 단정하고 사소한 / 판독 가능하다면 그것을 사용하고 그렇지 않으면 되돌립니다. 요구 사항이 변경되거나 LINQ 문이 추상화하는 논리를 통해 세부적으로 디버깅해야 할 경우 원래 비 LINQ 논리를 다시 작성하는 것이 번거로운 경우 LINQ하지 않고 길게 둡니다. 형태.
Ed Hastings

한 가지 일반적인 실수 foreach는 열거하는 동안 컬렉션에서 항목을 제거하는 것입니다. 일반적으로 for마지막 요소부터 루프가 필요합니다.
Slai

객체 지향 개발자를위한 Øredev 2013-Jessica Kerr – 기능적 원칙 에서 가치를 얻을 수 있습니다 . Linq는 33 분 후 곧 "Declarative Style"이라는 제목으로 프레젠테이션을 시작합니다.
치료

답변:


139

for vs. foreach

이 두 구성이 매우 유사하고 다음과 같이 상호 교환이 가능하다는 일반적인 혼동이 있습니다.

foreach (var c in collection)
{
    DoSomething(c);
}

과:

for (var i = 0; i < collection.Count; i++)
{
    DoSomething(collection[i]);
}

두 키워드가 모두 같은 세 글자로 시작된다는 것은 의미 상 유사하다는 것을 의미하지는 않습니다. 이 혼란은 특히 초보자에게 오류가 발생하기 쉽습니다. 컬렉션을 반복하고 요소로 무언가를하는 것은 다음과 같이 수행됩니다 foreach. for당신이하고있는 일을 정말로 알지 못한다면, 이 목적을 위해 사용될 필요는 없으며 사용되어서는 안됩니다 .

예를 들어 무엇이 잘못되었는지 봅시다. 마지막으로 결과를 수집하는 데 사용되는 데모 애플리케이션의 전체 코드를 찾을 수 있습니다.

이 예에서는 "Boston"을 만나기 전에 데이터베이스에서 이름을 기준으로 Adventure Works의 도시, 더 정확하게는 일부 데이터를로드합니다. 다음과 같은 SQL 쿼리가 사용됩니다.

select distinct [City] from [Person].[Address] order by [City]

데이터는를 ListCities()반환하는 메소드에 의해로드됩니다 IEnumerable<string>. 다음 foreach과 같은 모습입니다 :

foreach (var city in Program.ListCities())
{
    Console.Write(city + " ");

    if (city == "Boston")
    {
        break;
    }
}

for둘 다 상호 교환 가능하다고 가정하고을 사용하여 다시 작성하십시오 .

var cities = Program.ListCities();
for (var i = 0; i < cities.Count(); i++)
{
    var city = cities.ElementAt(i);

    Console.Write(city + " ");

    if (city == "Boston")
    {
        break;
    }
}

둘 다 같은 도시를 반환하지만 큰 차이가 있습니다.

  • 사용하는 경우 foreach, ListCities()한 번에 전화 (47 개) 항목을 산출한다.
  • 사용하는 경우 for, ListCities()94 회라는 전반적인 28,153 항목을 산출한다.

어떻게 된 거예요?

IEnumerable이다 게으른 . 결과가 필요한 순간에만 작업을 수행한다는 의미입니다. 게으른 평가는 매우 유용한 개념이지만 특히 결과가 여러 번 사용되는 경우 결과가 필요한 순간을 놓치기 쉽다는 사실을 포함하여 몇 가지주의 사항이 있습니다.

의 경우 foreach결과는 한 번만 요청됩니다. for 의 잘못 작성된 코드로 구현 된의 경우 결과는 94 번 , 즉 47 × 2로 요청됩니다 .

  • 매번 cities.Count()(47 회)

  • 매번 cities.ElementAt(i)(47 번)이라고합니다.

하나 대신 데이터베이스를 94 번 쿼리하는 것은 끔찍하지만 일어날 수있는 더 나쁜 것은 아닙니다. 예를 들어, select테이블 앞에 행을 삽입하는 쿼리가 쿼리 앞에 오는 경우 어떻게 될지 상상해보십시오 . 그래, 우리는 할 것이다 for데이터베이스를 호출하는 2,147,483,647 는 희망 전에 충돌하지 않는 한, 번.

물론 내 코드는 편향되어 있습니다. 나는 고의적으로 게으름을 사용하고 IEnumerable그것을 반복해서 부르는 방식으로 썼다 ListCities(). 초보자는 절대 그렇게하지 않을 것입니다.

  • IEnumerable<T>재산이없는 Count,하지만 방법을 Count(). 메소드 호출은 무섭고 결과가 캐시되지 않고 for (; ...; )블록에 적합하지 않을 것으로 예상 할 수 있습니다 .

  • 인덱싱을 사용할 수 없으며 LINQ 확장 방법 IEnumerable<T>을 찾는 것이 확실하지 않습니다 ElementAt.

아마 대부분의 초보자는 그 결과를 . ListCities()처럼 익숙한 것으로 변환했을 것입니다 List<T>.

var cities = Program.ListCities();
var flushedCities = cities.ToList();
for (var i = 0; i < flushedCities.Count; i++)
{
    var city = flushedCities[i];

    Console.Write(city + " ");

    if (city == "Boston")
    {
        break;
    }
}

여전히이 코드는 다른 코드와는 매우 다릅니다 foreach. 다시 말하지만 동일한 결과를 제공하며 이번에는 ListCities()메소드가 한 번만 호출 되지만 575 개의 항목이 생성되고을 사용하면 foreach47 개의 항목 만 생성됩니다.

차이는 사실에서 비롯 ToList()됩니다 모든 데이터가 데이터베이스에서로드 할 수 있습니다. foreach"Boston"이전의 도시 만 요청 했지만 새로운 for도시에서는 모든 도시를 검색하여 메모리에 저장해야합니다. 575 개의 짧은 문자열을 사용하면 큰 차이가 없을 것입니다. 그러나 수십억 개의 레코드가 포함 된 테이블에서 몇 개의 행만 검색하는 경우 어떻게됩니까?

그래서 foreach실제로 무엇입니까?

foreachwhile 루프에 더 가깝습니다. 이전에 사용한 코드 :

foreach (var city in Program.ListCities())
{
    Console.Write(city + " ");

    if (city == "Boston")
    {
        break;
    }
}

간단히 다음으로 대체 할 수 있습니다.

using (var enumerator = Program.ListCities().GetEnumerator())
{
    while (enumerator.MoveNext())
    {
        var city = enumerator.Current;
        Console.Write(city + " ");

        if (city == "Boston")
        {
            break;
        }
    }
}

둘 다 동일한 IL을 생성합니다. 둘 다 같은 결과를 얻습니다. 둘 다 동일한 부작용이 있습니다. 물론 이것은 while비슷한 무한대로 다시 작성할 수 for있지만 더 길고 오류가 발생하기 쉽습니다. 더 읽기 쉬운 것을 자유롭게 선택할 수 있습니다.

직접 테스트하고 싶습니까? 전체 코드는 다음과 같습니다.

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Diagnostics;
using System.Linq;

public class Program
{
    private static int countCalls;

    private static int countYieldReturns;

    public static void Main()
    {
        Program.DisplayStatistics("for", Program.UseFor);
        Program.DisplayStatistics("for with list", Program.UseForWithList);
        Program.DisplayStatistics("while", Program.UseWhile);
        Program.DisplayStatistics("foreach", Program.UseForEach);

        Console.WriteLine("Press any key to continue...");
        Console.ReadKey(true);
    }

    private static void DisplayStatistics(string name, Action action)
    {
        Console.WriteLine("--- " + name + " ---");

        Program.countCalls = 0;
        Program.countYieldReturns = 0;

        var measureTime = Stopwatch.StartNew();
        action();
        measureTime.Stop();

        Console.WriteLine();
        Console.WriteLine();
        Console.WriteLine("The data was called {0} time(s) and yielded {1} item(s) in {2} ms.", Program.countCalls, Program.countYieldReturns, measureTime.ElapsedMilliseconds);
        Console.WriteLine();
    }

    private static void UseFor()
    {
        var cities = Program.ListCities();
        for (var i = 0; i < cities.Count(); i++)
        {
            var city = cities.ElementAt(i);

            Console.Write(city + " ");

            if (city == "Boston")
            {
                break;
            }
        }
    }

    private static void UseForWithList()
    {
        var cities = Program.ListCities();
        var flushedCities = cities.ToList();
        for (var i = 0; i < flushedCities.Count; i++)
        {
            var city = flushedCities[i];

            Console.Write(city + " ");

            if (city == "Boston")
            {
                break;
            }
        }
    }

    private static void UseForEach()
    {
        foreach (var city in Program.ListCities())
        {
            Console.Write(city + " ");

            if (city == "Boston")
            {
                break;
            }
        }
    }

    private static void UseWhile()
    {
        using (var enumerator = Program.ListCities().GetEnumerator())
        {
            while (enumerator.MoveNext())
            {
                var city = enumerator.Current;
                Console.Write(city + " ");

                if (city == "Boston")
                {
                    break;
                }
            }
        }
    }

    private static IEnumerable<string> ListCities()
    {
        Program.countCalls++;
        using (var connection = new SqlConnection("Data Source=mframe;Initial Catalog=AdventureWorks;Integrated Security=True"))
        {
            connection.Open();

            using (var command = new SqlCommand("select distinct [City] from [Person].[Address] order by [City]", connection))
            {
                using (var reader = command.ExecuteReader(CommandBehavior.SingleResult))
                {
                    while (reader.Read())
                    {
                        Program.countYieldReturns++;
                        yield return reader["City"].ToString();
                    }
                }
            }
        }
    }
}

그리고 결과 :

--- for ---
Abingdon Albany 알렉산드리아 알함브라 [...] 본 보르도 보스턴

데이터는 94 회 호출되었고 28153 개의 아이템이 산출되었다.

--- 목록과 함께 ---
Abingdon Albany Alexandria Alhambra [...] 본 보르도 보스턴

데이터는 1 회 호출되었고 575 개의 아이템이 산출되었다.

--- 동안 ---
애 빙던 알바니 알렉산드리아 알함브라 [...] 본 보르도 보스턴

데이터를 1 회 호출하고 47 개의 아이템을 생성 하였다.

--- foreach ---
애 빙던 알바니 알렉산드리아 알함브라 [...] 본 보르도 보스턴

데이터를 1 회 호출하고 47 개의 아이템을 생성 하였다.

LINQ vs. 전통적인 방식

LINQ의 경우 C # FP가 아니라 Haskell과 같은 실제 FP 언어 인 FP ( function programming)배우고 싶을 수도 있습니다 . 기능적 언어는 코드를 표현하고 표현하는 특정 방법을 가지고 있습니다. 어떤 상황에서는 비 기능적 패러다임보다 우월합니다.

그것은 (나열 조작에 관해서 FP가 훨씬 뛰어난 것으로 알려져 목록 일반적인 용어, 관련이없는 것으로 List<T>). 이 사실을 감안할 때 C # 코드를 목록에 관해서보다 기능적으로 표현하는 기능은 오히려 좋은 것입니다.

확신이 없다면 이전 주제에 대한 기능적 및 비 기능적 방식으로 작성된 코드의 가독성을 비교하십시오 .


1
ListCities () 예제에 대한 질문입니다. 왜 한 번만 실행됩니까? 과거 수익률을 초과하는 데 문제가 없었습니다.
Dante

1
그는 IEnumerable에서 단 하나의 결과 만 얻을 것이라고 말하지는 않습니다. 그는 SQL 쿼리 (방법의 비싼 부분)가 한 번만 실행될 것이라고 말하고 있습니다-이것은 좋은 것입니다. 그런 다음 쿼리에서 모든 결과를 읽고 산출합니다.
HappyCat

9
@Giorgio :이 질문은 이해할 수 있지만, 초보자가 혼란 스러울 수있는 언어의 의미 론적 패더를 갖는 것은 우리에게 매우 효과적인 언어를 남기지 않을 것입니다.
Steven Evers

4
LINQ는 의미 상 설탕이 아닙니다. 지연된 실행을 제공합니다. 그리고 IQueryables (예 : Entity Framework)의 경우 쿼리가 반복 될 때까지 쿼리를 전달하고 구성 할 수 있습니다 (반환 된 IQueryable에 where 절을 추가하면 반복시 해당 where 절을 포함하도록 SQL이 서버에 전달됨) 필터링을 서버로 오프로드).
Michael Brown

8
이 답변이 마음에 드시면 예제가 다소 고안된 것 같습니다. 마지막 요약은 실제로 실제로 불일치가 의도적으로 코드가 깨진 결과 일 foreach때보 다 더 효율적 임을 시사합니다 for. 답의 철저 함은 스스로를 구속하지만, 우연한 관찰자가 어떻게 잘못된 결론을 내릴 수 있는지 쉽게 알 수 있습니다.
Robert Harvey

19

for와 foreach의 차이점에 대해서는 이미 큰 설명이 있습니다. LINQ의 역할에 대한 심각한 오해가 있습니다.

LINQ 구문은 구문 프로그래밍 설탕이 아니라 C #에 함수형 프로그래밍 근사치를 제공합니다. LINQ는 C #에 모든 이점을 포함한 기능적 구성을 제공합니다. LINQ는 IList 대신 IEnumerable을 반환하는 기능과 함께 반복 실행 지연을 제공합니다. 사람들이 일반적으로하는 일은 다음과 같이 함수에서 IList를 구성하고 반환하는 것입니다.

public IList<Foo> GetListOfFoo()
{
   var retVal=new List<Foo>();
   foreach(var foo in _myPrivateFooList)
   {
      if(foo.DistinguishingValue == check)
      {
         retVal.Add(foo);
      }
   }
   return retVal;
}

대신 yield return 구문 을 사용하여 지연된 열거를 만듭니다.

public IEnumerable<Foo> GetEnumerationOfFoo()
{
   //no need to create an extra list
   //var retVal=new List<Foo>();
   foreach(var foo in _myPrivateFooList)
   {
      if(foo.DistinguishingValue == check)
      {
         //yield the match compiler handles the complexity
         yield return foo;
      }
   }
   //no need for returning a list
   //return retVal;
}

이제 열거 형은 ToList하거나 반복 할 때까지 발생하지 않습니다. 그리고 그것은 필요할 때만 발생합니다 (여기서는 스택 오버플로 문제가없는 Fibbonaci의 열거입니다)

/**
Returns an IEnumerable of fibonacci sequence
**/
public IEnumerable<int> Fibonacci()
{
  int first, second = 1;
  yield return first;
  yield return second;
  //the 46th fibonacci number is the largest that
  //can be represented in 32 bits. 
  for (int i = 3; i < 47; i++)
  {
    int retVal = first+second;
    first=second;
    second=retVal;
    yield return retVal;
  }
}

피보나치 함수에 대해 foreach를 수행하면 46의 시퀀스가 ​​반환됩니다. 30을 원하면 계산됩니다.

var thirtiethFib=Fibonacci().Skip(29).Take(1);

람다 식에 대한 언어 지원 (IQueryable 및 IQueryProvider 구성과 결합 됨)을 통해 많은 재미를 얻을 수있는 곳은 다양한 데이터 세트에 대해 쿼리를 기능적으로 구성 할 수 있다는 것입니다. 소스의 기본 구성을 사용하여 쿼리 작성 및 실행). 여기에 간단한 세부 사항은 다루지 않지만 여기에 SQL 쿼리 공급자 를 만드는 방법을 보여주는 일련의 블로그 게시물이 있습니다.

요약하면 함수 소비자가 간단한 반복을 수행 할 때 IList보다 IEnumerable을 반환하는 것이 좋습니다. 또한 LINQ의 기능을 사용하여 복잡한 쿼리가 필요할 때까지 실행을 연기합니다.


13

하지만 코드가 점점 읽기 어려워지는 것을 볼 수 있습니다.

가독성은 보는 사람의 눈에 있습니다. 어떤 사람들은

var common = list1.Intersect(list2);

완벽하게 읽을 수 있습니다. 다른 사람들은 이것이 불투명하다고 말하고 선호합니다.

List<int> common = new List<int>();
for(int i1 = 0; i1 < list1.Count; i1++)
{
    for(int i2 = 0; i2 < list2.Count; i2++)
    {
        if (list1[i1] == list2[i2])
        {
            common.Add(i1);
            break;
        }
    }
}

수행중인 작업을보다 명확하게합니다. 더 읽기 쉬운 내용을 알려 드릴 수 없습니다. 그러나 내가 여기서 구성 한 예에서 내 편견 중 일부를 감지 할 수 있습니다 ...


28
솔직히 Linq는 의도를 객관적으로 읽기 쉽게 만들고 for 루프는 메커니즘을 객관적으로 읽기 쉽게 만듭니다.
jk.

16
For-for-if 버전이 교차 버전보다 더 읽기 쉽다는 것을 알려주는 사람으로부터 최대한 빨리 실행합니다.
Konamiman

3
@Konamiman-그것은 "가독성"을 생각할 때 사람이 찾는 것에 달려 있습니다. jk.의 의견은 이것을 완벽하게 보여줍니다. 루프는 쉽게 볼 수 있다는 점에서 더 읽을 수있는 방법 LINQ가 더 읽을 동안, 그 최종 결과를 얻고 무엇 최종 결과가 있어야한다.
Shauna

2
루프가 구현에 들어가는 이유는 어디에서나 교차를 사용하는 것입니다.
R. Martinho Fernandes

8
@Shauna : 몇 가지 다른 일을하는 메소드 내부에서 for-loop 버전을 상상해보십시오. 엉망입니다. 따라서 자연스럽게 자체 방법으로 분할합니다. 가독성 측면에서는 IEnumerable <T> .Intersect와 동일하지만 이제는 프레임 워크 기능을 복제하고 유지 관리하기 위해 더 많은 코드를 도입했습니다. 행동상의 이유로 커스텀 구현이 필요한 유일한 이유는 여기에서 가독성에 대해서만 이야기하는 것입니다.
Misko

7

LINQ와의 차이점은 foreach명령형과 선언 형의 두 가지 프로그래밍 스타일로 요약됩니다.

  • 명령형 :이 스타일에서는 컴퓨터에 "이 작업을 수행하십시오. 이제이 작업을 수행하십시오. 이제이 작업을 수행하십시오"라고 말합니다. 한 번에 한 단계 씩 프로그램을 공급합니다.

  • 선언적 :이 스타일에서는 컴퓨터에 원하는 결과를 알려주고 결과를 얻는 방법을 알아냅니다.

이 두 스타일의 전형적인 예는 어셈블리 코드 (또는 C)를 SQL과 비교하는 것입니다. 집회에서 당신은 한 번에 하나씩 문자 적으로 지시를합니다. SQL에서는 데이터를 결합하는 방법과 해당 데이터에서 원하는 결과를 표현합니다.

선언적 프로그래밍의 좋은 부작용은 약간 높은 수준의 경향이 있다는 것입니다. 이를 통해 코드를 변경하지 않고도 플랫폼이 진화 할 수 있습니다. 예를 들어 :

var foo = bar.Distinct();

여기서 무슨 일이 일어나고 있습니까? 고유 한 코어를 사용합니까? 두? 오십? 우리는 알지 못하고 신경 쓰지 않습니다. .NET 개발자는 코드 업데이트 후 코드가 마술처럼 빨라질 수있는 것과 동일한 목적을 계속 수행하는 한 언제든지 다시 작성할 수 있습니다.

이것이 기능적 프로그래밍의 힘입니다. 그리고 Clojure, F # 및 C # (기능 프로그래밍 사고 방식으로 작성)과 같은 언어로 된 코드가 명령형 코드보다 3 ​​배에서 10 배 더 작다는 것을 알게 될 것입니다.

마지막으로 C #에서는 대부분 데이터를 변경하지 않는 코드를 작성할 수 있기 때문에 선언적 스타일이 마음에 듭니다. 위의 예에서 Distinct()막대를 변경하지 않으면 새 데이터 사본이 반환됩니다. 이것은 막대가 무엇이든, 어디서든 갑자기 바뀌지 않았 음을 의미합니다.

다른 포스터들이 말하는 것처럼 함수형 프로그래밍을 배우십시오. 그것은 당신의 인생을 바꿀 것입니다. 가능하면 진정한 기능적 프로그래밍 언어로 수행하십시오. Clojure를 선호하지만 F #과 Haskell도 탁월한 선택입니다.


2
LINQ 처리는 실제로 반복 할 때까지 지연됩니다. var foo = bar.Distinct()본질적으로 또는 IEnumerator<T>전화 할 때까지 입니다. 이를 잘 모르면 버그를 이해하기 어려울 수 있기 때문에 중요한 차이점입니다. .ToList().ToArray()
Berin Loritsch

-5

팀의 다른 개발자가 LINQ를 읽을 수 있습니까?

그렇지 않으면 사용하지 않거나 두 가지 중 하나가 발생합니다.

  1. 코드를 유지할 수 없습니다
  2. 모든 코드와 코드에 의존하는 모든 것을 유지해야합니다.

각 루프에 대한 A는 목록을 반복하는 데 완벽하지만 이것이 당신이해야 할 일이 아니라면 그것을 사용하지 마십시오.


11
흠, 단일 프로젝트의 경우 이것이 답이 될 수 있지만, 중장기 적으로 직원을 훈련시켜야합니다. 그렇지 않으면 코드 이해력이 부족하여 좋은 생각처럼 들리지 않습니다.
jk.

21
실제로 일어날 수있는 세 번째 일이 있습니다. 다른 개발자들은 적은 노력을 기울이고 실제로 새롭고 유용한 것을 배울 수 있습니다. 들어 본 적이 없습니다.
Eric King

6
@InvertedLlama 개발자가 새로운 언어 개념을 이해하기 위해 정식 교육이 필요한 회사에 있었다면 새로운 회사를 찾는 것에 대해 생각할 것입니다.
Wyatt Barnett

13
아마도 라이브러리를 사용하여 그러한 태도를 벗어날 수는 있지만 핵심 언어 기능에 관해서는 그것을 자르지 않습니다. 프레임 워크를 선택하여 선택할 수 있습니다. 그러나 훌륭한 .NET 프로그래머는 언어 및 핵심 플랫폼 (System. *)의 모든 기능을 이해해야합니다. 그리고 Linq를 사용하지 않고 EF를 제대로 사용할 수 없다는 것을 고려할 때, 나는 .NET 프로그래머이고 Linq를 모른다면 요즘과 같은 시대에 무능합니다.
Timothy Baldridge

7
여기에는 충분한 다운 보트가 이미 있으므로 추가하지 않겠지 만 무지한 / 무능한 동료를 지원하는 주장은 결코 유효한 것이 아닙니다.
Steven Evers
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.