[44] 바인딩 된 변수는 수정하지 말라

다음 본문은 도서 이펙티브 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));
    }
}

람다 표현식에서 사용하는 모든 지역변수를 포함하는 중첩 클래스를 생성하는 것을 알 수 있다.

따라서 람다 내부에서든, 람다 외부에서든 완전히 동일한 필드에 접근하게 된다.

 

결론

 바인딩된 변수의 값을 수정하게되면 지연 수행과 클로저 특성 때문에 예상치 못한 문제가 발생할 수 있다.

 

 

 

 

댓글

Designed by JB FACTORY