아이템 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# 빌 와그너 저 - 한빛 미디어
'C# > Advanced C#' 카테고리의 다른 글
[Effective C#] 5장 예외 처리 (0) | 2021.11.27 |
---|---|
[Effective C#] 4장 LINQ 활용 (2) (0) | 2021.11.24 |
[Effective C#] 3장 제너릭 활용 (0) | 2021.11.20 |
[C#] Task (3) async / await 동작 더 알아보기 (0) | 2021.11.18 |
[Effective C#] 2장 .NET 리소스 관리 (0) | 2021.11.07 |