다음 본문은 도서 이펙티브 C# (빌 와그너)에서 나오는 주제를 다룹니다.
다음의 예는 클로저에서 캡처된 변수를 수정했을 때의 상황을 보여주기 위한 예이다.
var index = 0;
Func<IEnumerable<int>> sequence = () => Utilities.Generate(30, () => index++);
index = 20;
foreach(int n in sequence())
WriteLine(n);
WriteLine("Done");
index = 100;
foreach(var n in sequence())
WriteLine(n);
위의 코드를 실행하면 20부터 50까지를 출력한 후, 100부터 130까지를 출력한다.
C# 컴파일러는 쿼리 표현식을 실행 코드로 변환할 때 다양한 작업을 수행한다.
사용된 표현식이 어떤 형태인지에 따라 생성 방법이 다르다.
예시A (인스턴스 변수 접근 X, 지역변수에 접근X)
int[] someNumbers = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
var answers = from n in someNumbers
select n * n;
별다른 변수 참조가 없기 때문에 델리게이트를 통해 참조할 정적 메서드를 생성한다.
private static int HiddenFunc(int n) => (n * n);
private static Func<int, int> HiddenDelegateDefinition;
//
int[] someNumbers = new int[]{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
if(HiddenDelegateDefinition == null)
{
HiddenDelegateDefinition = new Func<int, int>(HiddenFunc);
}
var answers = someNumbers.Select<int, int>(HiddenDelegateDefinition);
예시B (인스턴스 변수에 접근O, 지역변수에 접근X)
public class ModeFilter
{
private readonly int modulus;
public ModeFilter(int mod)
{
modulus = mod;
}
public IEnumerable<int> FindVlaues(
IEnumerable<int> sequence)
{
return from n in sequence
where n % modulus == 0
select n * n;
}
}
인스턴스 변수를 참조하기 위해 델리게이트를 통해 참조하는 인스턴스 메서드를 생성한다.
public class ModeFilter
{
private readonly int modulus;
// 새롭게 추가된 메서드
private bool WhereClause(int n) => ((n % this.modulus) == 0);
// 기존 메서드
private static int SelectClause(int n) => (n * n);
// 기존 델리게이트
private static Func<int, int> SelectDelegate;
public IEnumerable<int> FindValues(IEnumerable<int> sequence)
{
if(SelectDelegate == null)
{
SelectDelegate = new Func<int, int>(SelectClause);
}
return sequence.Where<int>(
new Func<int, bool>(this.WhereClause)).
Select<int, int>(SelectClause);
}
}
예시C (인스턴스 변수에 접근X, 지역변수에 접근O)
public class ModFilter
{
private readonly int modulus;
public ModFilter(int mod)
{
modulus = mod;
}
public IEnumerable<int> FindValues(IEnumerable<int> sequence)
{
int numValue = 0;
return from n in sequence
where n % modulus == 0
select n * n / ++ numValues;
}
}
지역변수 혹은 메서드의 매개변수를 사용하는 코드가 포함될 경우 컴파일러가 상당한 추가 작업을 수행하게 된다.
이 경우 클로저가 필요하기 때문에 private로 중첩 클래스를 선언한다.
public class ModFilter
{
private sealed class Closure
{
public ModFilter outer;
public int numValues;
public int SelectClause(int n) => ((n*n) / ++ this.numValues);
}
private readonly int modulus;
public ModFilter(int mod)
{
this.modulus = mod;
}
private bool WhereClause(int n) => ((n % this.modulus) == 0);
public IEnumerable<int> FindValues(IEnumerable<int> sequence)
{
var c = new Closure();
c.outer = this;
c.numValues = 0;
return sequence.Where<int>(
new Func<int, bool>(this.WhereClause))
.Select<int, int>(
new Func<int, int>(c.SelectClause));
}
}
람다 표현식에서 사용하는 모든 지역변수를 포함하는 중첩 클래스를 생성하는 것을 알 수 있다.
따라서 람다 내부에서든, 람다 외부에서든 완전히 동일한 필드에 접근하게 된다.
결론
바인딩된 변수의 값을 수정하게되면 지연 수행과 클로저 특성 때문에 예상치 못한 문제가 발생할 수 있다.
'🌍 C# Study > 이펙티브 C#' 카테고리의 다른 글
[46] 리소스 정리를 위해 using과 try/finally를 활용하라 (2) | 2021.05.13 |
---|---|
[45] 메서드가 실패했음을 알리기 위해서 예외를 이용하라 (0) | 2021.05.12 |
[43] 쿼리 결과의 의미를 명확히 강제하고, Single()과 First()를 사용하라 (0) | 2021.05.12 |
[42] IEnumerable<T> 데이터 소스와 IQueryable<T> 데이터 소스를 구분하라 (0) | 2021.05.12 |
[41] 값비싼 리소스를 캡처하지 마라 (0) | 2021.05.03 |