Effective C# 4판 스터디, 그 네 번째 포스트

📚 목차

  • 🔸 ITEM 29: 컬렉션을 반환하기보다 이터레이터를 반환하는 것이 낫다
  • 🔸 ITEM 30: 루프보다 쿼리구문이 낫다.
  • 🔸 ITEM 31: 시퀀스에 사용할 수 있는 조합 가능한 API를 작성하라
  • 🔸 ITEM 32: Action, Predicate, Function과 순회 방식을 분리하라
  • 🔸 ITEM 33. 필요한 시점에 필요한 요소를 생성하라.
  • 🔸 ITEM 34. 함수를 매개변수로 사용하여 결합도를 낮춰라.
  • 🔸 ITEM 35. 확장 메서드는 절대 오버로드 하지마라.
  • 🔸 ITEM 36. 쿼리 표현식과 메서드 호출 구문이 어떻게 대응되는지 이해하라.
  • 🔸 ITEM 37. 쿼리를 사용할 때는 즉시 평가보다 지연 평가가 낫다.
  • 🔸 ITEM 38. 메서드보다 람다 표현식이 낫다.
  • 🔸 ITEM 39. function과 action 내에서는 예외가 발생하지 않도록 하라
  • 🔸 ITEM 40. 지연 수행과 즉시 수행을 구분하라
  • 🔸 ITEM 41. 값비싼 리소스를 캡처하지말라
  • 🔸 ITEM 42. IEnumerable<T> 데이터 소스와 IQueryable<T>이터 소스를 구분하라
  • 🔸 ITEM 43. 쿼리 결과의 의미를 명확히 강제하고 Single()First()를 사용하라




LINQ(Language-INtegrated Query)의 약자인 LINQ는 데이터 질의(Query) 기능을 C#에서 사용할 수 있는 기술이다.

쉽게 설명하자면 C#의 배열, 컬렉션, XML, DataSet 등등에서 내가 원하는 데이터만 가져오고 싶은 경우 사용할 수 있는 기술이다.

다음 예제는 배열에서 대문자 “A”로 시작하고 문자열의 길이가 3보다 큰 문자열을 LINQ로 추출한 뒤 콘솔에 출력합니다.

static void Main(string[] args)
{
    string[] strArr = { "Apple", "Banana", "Car", "Angular", "Add", "Sum" };

    var linqResult = from str in strArr
                        where str.StartsWith("A") && str.Length > 3
                        select str;

    foreach (var str in linqResult)
        Console.Write(str + " ");
}
Apple Angular

반복문과 조건문 없이 LINQ에서 사용할 수 있는 문법을 사용하여 원하는 문자열을 추출하였다.

LINQ는 C# 및 VB.net에서만 사용할 수 있다.

주요 기능

기능 설명
LINQ to objects 배열, 컬렉션, 제네릭 컬렉션에서 LINQ를 사용하여 원하는 데이터를 추출할 수 있다.
LINQ to XML XML 문서에서 LINQ를 사용하여 원하는 데이터를 추출할 수 있다.
LINQ to SQL SQL 서버의 데이터베이스와 함께 동작할 수 있다.
LINQ to DataSet DataSet에 LINQ를 사용하여 원하는 데이터를 추출할 수 있다.

LINQ의 장점

  • SQL과 유사하므로 SQL 사용이 익숙하다면, 쉽게 접근할 수 있다.
  • LINQ는 컴파일 시간에 타입을 체크한다. 따라서, 프로그램 실행 전에 문제가 되는 코드를 수정할 수 있다.
  • 반복문, 조건문을 사용하는 것보다 코드가 단순해진다.
  • LINQ의 질의는 재사용할 수 있다.

LINQ의 장점

  • RDBMS를 사용해보지 않은 개발자에게는 어려울 수 있다.
  • SQL과 유사하지만, 복잡한 질의(Query)는 작성할 수 없다.
  • 질의가 잘못된 경우 for, foreach와 같은 반복문을 사용하는 것보다 성능이 저하될 수 있다.




🔸 ITEM 29: 컬렉션을 반환하기보다 이터레이터를 반환하는 것이 낫다

이터레이터 메서드

  • 호출자가 시퀀스를 만들어내기 위해 yield return 을 사용하는 것을 말한다.

단한 예제로 아래 코드를 살펴보자. GetNumber() 라는 메서드는 3개의 yield return 문을 가지고 있다.

만약 외부에서 이 GetNumber()를 호출하게 되면, 첫번째 호출시에는 첫번째 yield return 10 을 실행하여 10을 리턴하게 되고,

두번째로 호출되면 yield return 20 이 실행되어 20을 리턴하게 된다.

이러한 방식으로 GetNumber()는 한꺼번에 10,20,30을 모두 리턴하는 것이 아니라,

한번 호출시마다 다음 yield return 문의 값을 리턴하는 것이다.

using System;
using System.Collections.Generic;

class Program
{
    static IEnumerable<int> GetNumber()
    {
        yield return 10;  // 첫번째 루프에서 리턴되는 값
        yield return 20;  // 두번째 루프에서 리턴되는 값
        yield return 30;  // 세번째 루프에서 리턴되는 값
    }

    static void Main(string[] args)
    {
        foreach (int num in GetNumber())
        {
            Console.WriteLine(num);
        }             
    }
}

이러한 특별한 리턴 방식은 다음과 같은 경우에 유용하게 사용된다.

  • 만약 데이타의 양이 커서 모든 데이타를 한꺼번에 리턴하는 것하는 것 보다 조금씩 리턴하는 것이 더 효율적일 경우. 예를 들어, 어떤 검색에서 1만 개의 자료가 존재하는데, UI에서 10개씩만 On Demand로 표시해 주는게 좋을 수도 있다. 즉, 사용자가 20개를 원할 지, 1000개를 원할 지 모르기 때문에, 일종의 지연 실행(Lazy Operation)을 수행하는 것이 나을 수 있다.

  • 어떤 메서드가 무제한의 데이타를 리턴할 경우. 예를 들어, 랜덤 숫자를 무제한 계속 리턴하는 함수는 결국 전체 리스트를 리턴할 수 없기 때문에 yield 를 사용해서 구현하게 된다.

  • 모든 데이타를 미리 계산하면 속도가 느려서 그때 그때 On Demand로 처리하는 것이 좋은 경우. 예를 들어 소수(Prime Number)를 계속 리턴하는 함수의 경우, 소수 전체를 구하면 (물론 무제한의 데이타를 리턴하는 경우이기도 하지만) 시간상 많은 계산 시간이 소요되므로 다음 소수만 리턴하는 함수를 만들어 소요 시간을 분산하는 지연 계산(Lazy Calculation)을 구현할 수 있다.




🔸 ITEM 30: 루프보다 쿼리구문이 낫다

System.LINQ 네임스페이스에 포함된 쿼리 구문은 IEnumerable<T>에 대한 확장 인터페이스를 사용한다.

select, from, where, group by, order by와 같은 쿼리 구문을 사용할 수 있으며 C# 컴파일러가 일반 확장 메서드로 해석해준다.

쿼리 구문을 사용하면 복잡한 전처리 과정을 간단하게 쿼리식으로 표현가능하며 가독성이 높아지는 장점이 있다.

내부적으로는 IEnumerable<T>를 반환하는 이터레이터 메서드 처럼 동작하여 지연된 평가값이 반환된다.

private static IEnumerable<Tuple<int, int>> ProduceIndices()
{
    var storage = new List<Tuple<int, int>>();

    for(var x = 0; x < 100; ++x)
    {
        for(var y = 0; y < 100; ++y)
        {
            if(x+y<100)
            {
                storage.Add(Tuple.Create(x, y));
            }
        }
    }

    storage.Sort((p1, p2) => (p2.Item1 * p2.Item1 + p2.Item2 * p2.Item2)
                .CompareTo((p1.Item1 * p1.Item1 + p1.Item2 * p1.Item2)));

    return storage;
}

private static IEnumerable<Tuple<int, int>> QueryIndices()
{
    return from x in Enumerable.Range(0, 100)
            from y in Enumerable.Range(0, 100)
            where x + y < 100
            orderby (x * x + y * y) descending
            select Tuple.Create(x, y);
}




🔸 ITEM 31: 시퀀스에 사용할 수 있는 조합 가능한 API를 작성하라

반복 구문이 필요한 경우는 대부분 단일 요소가 아니라 시퀀스(컬렉션)를 처리하는 알고리즘을 작성하는 경우가 많다.

이 때, foreach 혹은 for, while을 사용하게 되는데, 이를 사용할 때에는 매개변수로 컬렉션을 받아와서

컬렉션에 포함된 요소들을 살펴보거나, 내용을 수정하거나,

혹은 그 중 일부만 필터링해서 또 다른 컬렉션에 그 결과를 저장한 후 반환하는 식으로 작성한다.

이는 효율성에 좋지않다. 그 이유는 전체 컬렉션을 대상으로 하나의 작업만을 하는 것이 아니고

여러 작업 끝에 원하는 결과물을 얻을 수 있는 경우가 대부분이기 때문이며,

이 작업에 대한 중간 결과물을 저장할 컬렉션이 필요할 수도 있다(큰 용량의 컬렉션일 수도 있다.)


또한 하나의 컬렉션 전체에 대해 작업을 할 때 이 작업이 끝나기 전까지는 두 번째 작업을 못하게 되는데,

이러한 작업을 하는 경우에는 대부분 각 작업마다 전체 컬렉션을 매번 순회해서 하기 때문에,

다단계로 구성된 알고리즘을 수행하는 경우 전체적으로 수행 시간이 길어질 수 밖에 없다.

따라서 메서드를 각각의 작업별로 나누고 메서드를 호출하게 되면 중간 저장소도 필요없게 되고, 속도도 개선되지만,

이렇게 되면 함수의 재사용성이 낮아진다.


따라서, 전 챕터에서도 말했듯이 시퀀스를 다루는 메서드를 사용할 때는 C# 이터레이터를 사용하는 것이 좋다.

이는 출력도 출력시퀀스에서 하고 싶을 때 할 수 있고, 입력의 경우에도 입력시퀀스에서 올바른 순서의 정보를 가져온다.

public static IEnumerable<int> Unique(IEnumerable<int> nums)
{
    var uniqueVals = new HashSet<int>();
	
    foreach (var num in nums)
    {
    	if (!uniqueVals.Contains(num))
        {
        	uniqueVals.Add(num);
        	yield return num;
        }
    }
}




🔸 ITEM 32: Action, Predicate, Function과 순회 방식을 분리하라

대리자를 이용하여 이터레이터 메서드와 같은 함수에서 사용되는 처리 기능을 분리하여 사용하는 것이 좋다.

Where<T> : 시퀀스 중 Predicate<T> 함수를 통과하는 원소만 반환한다.

public static IEnumerable<T> Where<T>(IEnumerable<T> sequence, Predicate<T> filterFunc)
{
	if (filterFunc == null)
    	throw new ArgumentNullException("Predicate must not be null");
        
    foreach (T item in sequence)
    	if (filterFunc(item))
        	yield return item;
}

Select : 시퀀스 원소를 순회하여 새로운 원소를 반환할 수 있다.

public static IEnumerable<T out> Select<T in, T out>(IEnumerable<T in> sequence, Func<T in, T out> method)
{
	foreach (T element in sequence)
		yield return method(element);
}




🔸 ITEM 33. 필요한 시점에 필요한 요소를 생성하라

이터레이터 메서드를 활용해서 컬렉션을 생성하는 것을 적극 고려한다.

이터레이터 메서드는 지연 평가로 원소가 생기는 시점을 나중으로 미루거나 ToList()와 같이 즉시 수행하도록 선택할 수 있다.




🔸 ITEM 34. 함수를 매개변수로 사용하여 결합도를 낮춰라.

델리게이트를 사용하여 컴포넌트의 계약을 기술하면 클라이언트 측에서 코드를 사용하기가 쉬워진다.

다른 개발자가 사용할 코드를 작성하는 경우 의존성 문제를 코드에서 분리하는 것은 상당히 까다롭다.

이때 함수를 매개변수로 사용하면 사용하는 측과 구현하는 측의 코드를 분리할 수 있다.

단점은 코드의 복잡도가 올라가고 비용도 증가하게 된다.

아래의 예시는 시퀀스를 결합하는 Zip 메서드이다.

public static IEnumerable<string> Zip(IEnumerable<string> first, IEnumerable<string> second)
{
    using(var firstSequence = first.GetEnumerator())
    {
        using(var secondSequence = second.GetEnumerator())
        {
            while(firstSequence.MoveNext() && secondSequence.MoveNext())
                yield return $"{firstSequence.Current} {secondSequence.Current}";
        }
    }
}

이 메서드를 조금 수정하여 함수를 매개변수로 받게 개선할 수 있다.

public static IEnumerable<TResult> Zip<T1, T2, TResult>(
    IEnumerable<T1> first, 
    IEnumerable<T2> second,
    Func<T1, T2, TResult> zipper)
{
    using (var firstSequence = first.GetEnumerator())
    {
        using (var secondSequence = second.GetEnumerator())
        {
            while (firstSequence.MoveNext() && secondSequence.MoveNext())
                yield return zipper(firstSequence.Current, secondSequence.Current);
        }
    }
}

사용하는 측에서는 다음과 같이 람다식을 사용할 수 있다.

List<int> first = new List<int>() { 0, 1, 2, 3 };
List<string> second = new List<string>() { "A", "B", "C", "D" };
var result = Zip(first, second, (one, two) => $"{one} {two}");

하지만 이렇게 결합도를 느슨하게 구성하려면 오류를 처리하기 위해서 추가적인 작업이 필요하다.

이벤트를 발생시킬 때 해당 이벤트가 null인지 확인해야하고,

델리게이트가 null 인경우 이를 위해 기본동작을 마련해야할지 등 여러 생각할 거리가 생긴다.

즉, 예측가능한 수준에서 델리게이트를 잘 사용한다면 명시성은 조금 희생하는 대신 유연성을 취할 수 있을 것이다.




🔸 ITEM 35. 확장 메서드는 절대 오버로드 하지마라.

확장 메서드의 목적은 기존에 개발된 타입을 개선하기 위함이다.

타입의 본질적인 동작 방식을 변경하기 위하여 사용해서는 안된다.

확장 메서드를 과도하게 사용하거나 잘못 사용하면 유지보수 비용이 급격하게 증가하게 된다.

다음은 여러 네임스페이스에 같은 이름의 확장메서드를 선언한 코드이다.

namespace ConsoleExtensions
{
    public static class ConsoleReport
    {
        public static string Format(this Person target) =>
            $"{target.LastName, 20} {target.FirstName, 15}";
    }
}

namespace XmlExtensions
{
    public static class XmlReport
    {
        public static string Format(this Person target) =>
            new XElement("Person",
                new XElement("LastName", target.LastName),
                new XElement("FirstName", target.FirstName)
                ).ToString();
    }
}

위의 코드는 다음과 같은 문제점이 있다.

  1. 네임스페이스를 잘못참조하면 프로그램 동작 방식이 바뀐다.

  2. 2개의 네임스페이스가 공통적으로 필요한 메서드는 중복 정의할 수밖에 없다.

  3. 코드 사용자가 네임스페이스가 바뀐다고 코드의 동작이 바뀐다는 것을 인지하지 못한다.

위의 예시는 사실 객체의 기능을 확장한 게 아니라 그냥 외부에서 객체를 사용하였다고 보는 게 맞을 것이다.

특정 타입에 대해 동작하는 확장 메서드는 하나의 세트로 간주해야 한다.

확장 메서드를 여러 네임 스페이스에 오버로드 해서는 안된다.

동일한 확장 메서드를 여러 개 만들어야 한다면 이름을 달리하고 정적 메서드로 작성하는 걸 고려하자.




🔸 ITEM 36. 쿼리 표현식과 메서드 호출 구문이 어떻게 대응되는지 이해하라

C# 컴파일러는 LINQ의 쿼리 표현식과 메서드 호출 구문으로 변환한다.


🔹 Where

필터 이상의 역할을 수행하지 않는다.

where n < 5
numbers.Where(n => n < 5);


🔹 Select

입력값을 다른 타입으로 변환하는 용도로 사용된다.

입력 시퀀스 내의 개별 요소에 대해 각기 새로운 타입의 객체를 생성하여 출력 시퀀스로 내보내지만 요소들을 수정해서는 안된다.

var squares = from n in numbers
              select new { Number = n, Square = n * n };
var squares = numbers.Select(n =>
              new { Number = n, Square = n * n });


🔹 OrderBy, ThenBy

입력값을 정렬한다.

var people = from e in employees
             where e.Age > 30
             orderby e.LastName, e.FirstName, e.Age
             select e;
var people = employees.Where(e => e.Age > 30).
             OrderBy(e => e.LastName).
             ThenBy(e => e.FirstName).
             ThenBy(e => e.Age);


orderby절을 각각 쓰면 ThenBy가 아니라 OrderBy를 각각 실행돼버려서 앞 두 개는 의미 없어지니 주의.

var people = from e in employees
             where e.Age > 30
             orderby e.LastName, e.FirstName, e.Age
             select e;
var people = employees.Where(e => e.Age > 30).
             OrderBy(e => e.LastName).
             ThenBy(e => e.FirstName).
             ThenBy(e => e.Age);


내림차순 정렬은 다음과 같이 지정할 수 있다.

var people = from e in employees
             where e.Age > 30
             orderby e.LastName decending, e.FirstName, e.Age
             select e;


🔹 group into

개별 요소로 하나의 키와 값의 리스트를 쌍으로 갖는 시퀀스를 생성한다.

var results = from e in employees
              group e by e.Department into d
              select new
              {
                  Department = d.Key,
                  Size = d.Count()
              };
var results = from d in
              from e in employees group e by e.Department
              select new {
              Department = d.Key,
              Size = d.Count()};

이를 메서드 호출 구문으로 변환한다.

var results = employees.GroupBy(e => e.Department).
                       Select(d => new { Department = d.Key, Size = d.Count() });


🔸 ITEM 37. 쿼리를 사용할 때는 즉시 평가보다 지연 평가가 낫다

쿼리를 정의하더라도 결과 데이터나 시퀀스를 즉각 얻어 오는 것은 아니다.

쿼리 정의는 작업 절차를 정의한 것일 뿐이다.

실제로 쿼리의 결과를 이용하여 순회를 수행해야만 결과가 생성된다.

다음은 쿼리의 결과를 대상으로 추가적인 쿼리를 수행하는 코드이다.

var sequence1 = Getnerate(10, () => DateTime.Now);
var sequence2 = from value in sequence1
                select value.ToUniversalTime();

sequence2를 순회할 때 sequence1이 이미 생성해둔 값을 순회하면서 개별 요소를 수정하는 것이 아니라,

순회 시점에 맞춰 sequence1이 값을 생성한다.

하지만 일부 쿼리 표현식은 결과를 얻기 위해서 반드시 전체 시퀀스가 필요한 경우가 있다.

where, orderby, Max, Min은 전체 시퀀스를 요구한다.

다음의 식은 영원히 수행 (혹은 int.MaxValue까지) 수행된다.

쿼리 구문을 수행하기 위해서 시퀀스 내의 모든 값을 대상으로 비교 연산을 수행하기 때문이다.

static void Main(string[] args)
{
    var answers = from number in AllNumbers()
                    where number < 10
                    select number;
                    
    foreach(var num in answers)
        Console.WriteLine(num);
}

static IEnumerable<int> AllNumbers()
{
    var number = 0;
    while(number < int.MaxValue)
    {
        yield return number++;
    }
}

이를 해결하기위해 다음과 같이 고쳐 써볼 수 있다.

Take()는 시퀀스로부터 처음 N개의 객체를 반환한다.

static void Main(string[] args)
{
    var answers = from number in AllNumbers()
                    select number;
                    
    var smallNumbers = answers.Take(10));
    foreach(var num in smallNumbers)
        Console.WriteLine(num);
}

static IEnumerable<int> AllNumbers()
{
    var number = 0;
    while(number < int.MaxValue)
    {
        yield return number++;
    }
}

📌 전체 시퀀스를 사용하는 메서드를 사용할 때 주의사항

  1. 시퀀스가 무한정 지속될 가능성이 있다면 이 같은 메서드를 사용할 수 없다.
  2. 시퀀스가 무한이 아니더라도 필터링은 다른 쿼리보다 먼저 수행하는 것이 좋다. 개별 요소의 개수가 줄어 다음으로 수행할 쿼리의 성능을 개선할 수 있기 때문이다.
// 정렬 후 필터링
var sortedProjuctsSlow = from p in products
                         orderby p.UnitsInStock descending
                         where p.UnitsInStock > 100
                         select p;
                         
// 필터링 후 정렬
var sortedProjuctsSlow = from p in products
                         where p.UnitsInStock > 100
                         orderby p.UnitsInStock descending
                         select p;




🔸 ITEM 38. 메서드보다 람다 표현식이 낫다.

람다 표현식을 사용하여 코드를 작성하면 동일한 코드를 반복하게 될 때가 있다.

// 20년 이상 근속자
var earlyFolks = from e in allEmployees
                 where e.Classification == EmployeeType.Salary
                 where e.YearsOfService >= 20
                 where e.MonthlySalary < 4000
                 select e;

// 20년 미만 근속자
var newest = from e in allEmployees
                 where e.Classification == EmployeeType.Salary
                 where e.YearsOfService < 20
                 where e.MonthlySalary < 4000
                 select e;

where절을 여러 번에 걸쳐 나누어 썼기 때문에

성능을 우려해 하나의 where절로 변경을 하겠는가?

하지만 단순 조건문은 인라인화 될 가능성이 높기 때문에 성능이 개선되기를 기대하기는 어렵다.

여러번에 where절에 나눠 쓰는 것이 명시적이라면 블록으로 두는 편이 낫다.

하지만 두 람다식에서 중복되는 람다식이 눈에 띈다. 이를 메서드로 분리하면 어떨까?

private static bool LowPaidSalaried(Employee e) =>
    e.MonthlySalary < 4000 && e.Classification ==
    EmployeeType.Salary;
    
var earlyFolks = from e in allEmployees
                 where LowPaidSalaried(e) &&
                         e.YearsOfService >= 20
                  select e;
                  
var earlyFolks = from e in allEmployees
                 where LowPaidSalaried(e) &&
                         e.YearsOfService < 20
                  select e;

좀 단순해진 것 같지만, 분리된 메서드는 재사용될 가능성이 현저히 낮다.

동일하게 사용되는 로직을 분리하여 호출 체인의 앞쪽으로 이동시켜보면 해결될 것이다.

그렇다면 다음과 같이 재사용 가능한 빌딩 빌딩 블록을 만들어 볼 수 있다.

private static IQueryable<Employee> LowPaidSalariedFilter
    (this IQueryable<Employee> sequence) =>
      from s in sequence
      where s.Classification == EmployeeType.Salary &&
        s.MonthlySalary < 4000
      select s;
    
var salaried = allEmployees.LowPaidSalariedFilter();

var earlyFolks = salaried.Where(e => e.YearsOfService >= 20);
var newes = salaried.Where(e => e.YearsOfService < 20);




🔸 ITEM 39. function과 action 내에서는 예외가 발생하지 않도록 하라

일련의 값을 순차 처리하는 코드에서 중간지점에서 오류가 난다면 예외 발생 지점을 파악하기 어렵다.

특히나 시퀀스 내 요소의 값을 직접 수정하는 경우 루틴 수행 이전으로 원복 하기는 매우 어렵다.

메서드가 절대로 예외를 유발하지 않도록 하려면 어떻게 해야할까?

우선 생각해볼 수 있는 건 단순하게 예외가 발생할 상황을 필터링하는 것이다.

allEmployees.FindAll(
    e => e.Classification == EmployeeType.Active).
    ForeEach(e => e.MonthlySalary *= 1.05M);

하지만 이는 예상 가능한 오류에 대해서만 처리가 가능하다.

예외가 발생하지 않도록 작성하는 것이 불가능한 경우라면,

원본 시퀀스의 복사본으로 알고리즘을 수행하고 정상 완료된 경우에 시퀀스를 대체하는 방법이 있다.

var updates = (from e in allEmployees
               select new Employee
               {
                   EmployeeID = e.EmployeeID,
                   Classification = e.Classification,
                   YearsOfService = e.YearsOfService,
                   MonthlySalary = e.MonthlySalary *= 1.05M
               }).ToList();

이 경우 코드의 양이 늘어나며 성능에도 영향을 미칠 수 있지만,

기존의 데이터를 수정하는 대신 새로운 데이터를 생성하도록 작성한다면

작업 완료를 확인할 기회가 생길 뿐만 아니라 문제가 생길 경우 프로그램의 상태를 변경하지 않기에 안전하다.




🔸 ITEM 40. 지연 수행과 즉시 수행을 구분하라

🔹 명령형 코드 (Imperative Code)

  • 어떻게 작업을 수행해야 하는지를 단계별로 세분화하여 기술한다.

  • 필요한 매개변수를 모두 계산한 다음에야 비로소 메서드를 호출한다.

  • 아래의 예시에서는 항상 모든 메서드를 호출하며, 각 메서드의 부수효과는 반드시 한 번씩만 발생한다.

  • 메서드를 호출하고 그 결과를 다른 메서드에 전달한다.

var answer = DoStuff(Method1(), Method2(), Method3());

🔹 선언적 코드 (Declarative Code)

  • 해결석이며 무슨 작업을 해야 하는지를 정의한다.

  • 각 메서드의 수행 결과가 필요한 경우에만 호출된다.

  • 아래의 예시에서는 메서드가 각기 호출될 수도 있고 호출되지 않을 수 있으며, 여러 번 호출될 수도 있다.

  • 델리게이트를 매개변수로 전달한다.

var answer = DoStuff(() => Method1(), () => Method2(), () => Method3());

🔹 명령적 코드 vs 선언적 코드

호출 시점의 차이가 발생하기 때문에 명령형 코드(즉시 수행)와 선언적 코드(지연 수행)의 두 가지 방식을 섞어서 사용하면 문제를 일으킬 수 있다.




🔸 ITEM 41. 값비싼 리소스를 캡처하지말라

개발자는 일반적으로 블록을 벗어나면 지역변수가 가비지 콜렉터에 의해 정리될 것이라 생각하여

지역변수의 수명을 거의 신경쓰지 않는다.

하지만 클로저(Closure)는 이러한 규칙을 벗어난다.

캡처된 변수를 사용하는 마지막 델리게이트가 가비지화될 때 까지 해당 변수는 가비지로 간주되지 않는다.

일반적으로 단순 메모리 리소스만 사용한다면 적절한 시점에 가비지로 수집될 것이기 때문에 신경쓸 필요없지만

매우 무거운 리소스를 참고하고 있을경우 더욱 신경써야 한다.




🔸 ITEM 42. IEnumerable<T> 데이터 소스와 IQueryable<T>이터 소스를 구분하라

IEnumerable<T>IQueryable<T>는 거의 동일한 API정의를 가진다.

대부분의 경우 두 인터페이스는 상호 교환이 가능하다. 하지만 사실 이 둘은 동작 방식도 다르고 성능도 크게 차이 난다.

// IQueryable<T>
var q = from c in dbContext.Customers
        where c.City == "London"
        select c;
        
var finalAnswer = from c in q
                    orderby c.Name
                    select c;

// IEnumerable<T>    
var q = (from c in dbContext.Customers
         where c.City == "London"
         select c).AsEnumerable();
     
var finalAnswer = from c in q
                  orderby c.Name
                  select c;

위 코드의 결과는 동일하지만 동작 방식은 상이하다.

첫 번째 예는 일반적인 LINQ to SQL 쿼리이며 IQueryable<T>의 기능을 사용한다.

LINQ to SQL 라이브러리가 모든 쿼리문을 결합하여 단번에 SQL 결과를 생성한다. 단 한차례 데이터베이스를 호출한다.

두 번째 예는 데이터베이스 객체를 IEnumerable<T> 시퀀스로 변경하기 때문에 데이터베이스가 아니라 로컬에서 더 많은 작업을 수행하게 된다.

쿼리문이 IEnumerable<T> 시퀀스를 반환하므로 그다음 작업은 LINQ to Objects 구현체와 델리게이트를 이용하여 수행된다.

첫 번째 쿼리문이 수행되면 데이터베이스에 쿼리를 전달하여 City값이 London인 모든 레코드를 가져온다. 이후 로컬 머신에서 Name 필드에 따라 정렬한다.

**대부분의 경우 쿼리 작업을 수행할 때 IEnumerable보다는 IQueryable를 사용하는 편이 훨씬 효율적이다.**




🔸 ITEM 43. 쿼리 결과의 의미를 명확히 강제하고 Single()First()를 사용하라

🔹 Single

LINQ에서 SIngle()은 정확히 1개의 요소만 반환한다.

만약 어떤 요소도 포함되지 않거나 여러 개의 요소가 포함되는 경우 Single()은 예외를 유발한다.

결괏값으로 한 개밖에 존재하지 않는다는 것을 강력히 제한하고 싶을 때 사용하자.

SingleOrDefault()

어떤 요소도 포함되지 않을경우 기본값을 반환(참조 타입은 null)한다. 하지만 역시 결괏값이 2개 이상이면 예외를 일으킨다.


🔹 First

여러 개의 값이 반환되는 경우 첫 번째 요소만을 가져온다. 역시 어떤 요소도 포함되지않는다면 예외를 발생시킨다.

FirstOrDefault()

첫번째 요소를 반환하며, 어떤 요소도 포함되지 않을 경우 기본값을 반환(참조 타입은 null)한다.

원하는 한개의 요소 가져오기

당연히 단순히 한개의 요소를 가져오는 것으로 해결되지 않는 경우가 많기 때문에

First()를 실행하기 전 첫 번째 위치로 옮기기 위해서 정렬을 수행하는 것도 하나의 방법이다.

특정 위치의 요소 가져오기

Skip()First()를 함께 사용하여 원하는 요소를 가져올 수 있다. 하지만 요소의 총개수가 3개 이상이라는 가정이 있다.

var answer = (from p in Forwards
              where p.GoalsScored > 0
              orderby p.GoalsScored
              select p).Skip(2).First(); // 세번째 요소를 반환한다.