C#/Advanced C#

[Effective C#] 4장 LINQ 활용 (1)

로파이 2021. 11. 24. 22:02

아이템 29. 컬렉션을 반환하기보다 이터레이터를 반환하는 것이 낫다

 

이터레이터 메서드

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

public static IEnumerable<char> GenerateAlphabet()
{
    var letter = 'a';
    while(letter <= 'z')
    {
    	yield return letter;
    	++letter;
    }
}

이터레이터 메서드는 해당 메서드를 호출할 때 다음 원소를 생성하여 반환한다.

단일 호출로 전체 시퀀스가 만들어지지 않기 때문에 실제 시퀀스를 만들때 까지 지연시키는 효과가 있다.

다음과 같이 Enumerable<T>의 foreach를 이용하여 순회할 때 실제 원소가 구성된다.

foreach(var element in GenerateAlphabet())
	Console.WriteLine($"Element : {element}");

 

이터레이터 메서드를 사용하는 이유

  • 시퀀스의 모든 원소를 한번에 만드는 것이 아니라 최종 시퀀스의 원소는 여러 과정을 거쳐 만드는 것을 가정할 때 이터레이터 메서드를 사용하는 것이 유리하다.
  • 만약 즉시 평가된 컬렉션을 원한다면, ToList()나 ToArray()와 같은 확장 메서드를 이용하여 IEnumerable<T> 타입을 이용하는 시퀀스로부터 모든 항목이 담긴 컬렉션을 손쉽게 생성할 수 있다.

 

아이템 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);
        }

 

아이템 31. 시퀀스에 사용할 수 있는 조합 가능한 API를 작성하라

 

IEnumerable<T> 시퀀스를 입력으로 하여 IEnumerable<T>를 반환하는 이터레이터 메서드를 적극 활용한다. 이터레이터 메서드는 컬렉션의 기존 원소에 대해 값을 변경, 추가, 제거 혹은 새로운 타입의 원소를 만들수 있다. 

이러한 처리 과정을 각 단계별로 분리해서 이터레이터 메서드를 정의하면 재사용과 관리하기가 쉬워지고 IEnumerable<T>를 이용하여 지연된 평가의 장점을 그대로 사용할 수 있다.

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;
        }
    }
}

 

아이템 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<T> : 시퀀스 원소를 순회하여 새로운 원소를 반환할 수 있다.

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

 

아이템 33. 필요한 시점에 필요한 요소를 생성하라

 

이터레이터 메서드를 활용해서 컬렉션을 생성하는 것을 적극 고려한다. 이터레이터 메서드는 지연 평가로 원소가 생기는 시점을 나중으로 미루거나 ToList()와 같이 즉시 수행하도록 선택할 수 있다.

 

아이템 34. 함수를 매개변수로 사용하여 결합도를 낮추라

 

함수를 대리자 형식의 객체 Action, Func, Predicate를 적극 활용해서 해당 메서드의 기능을 분리하는 것이 좋다.

 

ex) 컬렉션의 특정 원소에 대해 일정 조건 Predicate<T> 만 만족하는 원소로 새로 구성된 컬렉션을 반환한다.

 

결합도를 낮추는 것은 해당 메서드에서 분리될 수 있는 기능을 최대한 나눔으로써 각각 분리된 메서드와 기능(대리자와같은)을 따로 관리할 수 있으며 재사용하기도 쉽다. 하지만 기능의 분리로 코드의 복잡성이 증가하고 한눈에 모든 기능을 파악하기 어렵다는 단점이 있다.

 

두 string 컬렉션을 사용하여 두 원소를 이용하여 새로운 string으로 만드는 ZipAsText

        private static IEnumerable<string> ZipAsText(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}";
                    }
                }
            }
        }

 Zip이라는 제너릭 메서드는 제너릭 원소 타입을 받고 입력 처리기도 Func 대리자로 받아 기능을 분리한다.

 private 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);
                    }
                }
            }
        }

 

vs 인터페이스 사용하기

인터페이스는 클래스 계층 구조를 강제하지 않으면서 느슨한 결합을 만들 수 있다.

기존 상속으로 활용할 수 있는 베이스 클래스의 기능 재사용을 할 수 없다는 단점이 있다.

 

아이템 35. 확장 메서드는 절대 오버로드하지 마라

 

- 인터페이스를 사용할 때 주의할 점

인터페이스에는 최소한의 기능만을 정의하고 확장 메서드를 이용하여 공통적으로 사용될 것으로 예상되는 작업에 대한 기본 구현체를 제공할 수 있다.

 

네임스페이스만 달리하여 다른 기능을하는 이름이 같은 두 함수가 있다하자

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

// XmlExtensions.cs
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();
    
    
    }
}

 두 어셈블리에서 Person 클래스에 대한 확장 메서드를 제공하고 있다. 사용자에게 적절한 네임스페이스를 포함하여 기능을 선택할 것으로 넘기고 있는데, 네임 스페이스의 참조는 여러 파일에서 복잡하게 얽힐 수 있다. 네임스페이스 오류로 사용 실수를 저지를 수 있으며 따라서 네임 스페이스로 둘을 분리하는 것은 허약하다.

 

확장 메서드는 추가하려는 기능이 자연스럽게 타입과 어울릴 때만 사용해야한다. 어떠한 인터페이스 기능을 타입에 맴버 함수로 추가했을 때 자연스러운 것만 고려하라는 뜻이다.

 

두 확장 메서드를 지원하고 싶다면 같은 정적 클래스 내에서 다른 이름으로 정의해주는 것이 좋다.

namespace PersonReportExtensions
{
    public static class PersonReports
    {
    	public static string FormatAsText(this Person target) => new XElement("Person", new XElement("LastName", target.LastName),
                                                           new XElement("FirstName", target.FirstName)).ToString();
    
        public static string FormatAsXML(this Person target) => $"{target.LastName, 20}, {target.FirstName, 15}"
    }
}

 

출처 : Effective C# 빌 와그너 저 - 한빛 미디어