ReSharper가 "암시 적으로 캡처 된 클로저"라고 말하는 이유는 무엇입니까?


296

다음 코드가 있습니다.

public double CalculateDailyProjectPullForceMax(DateTime date, string start = null, string end = null)
{
    Log("Calculating Daily Pull Force Max...");

    var pullForceList = start == null
                             ? _pullForce.Where((t, i) => _date[i] == date).ToList() // implicitly captured closure: end, start
                             : _pullForce.Where(
                                 (t, i) => _date[i] == date && DateTime.Compare(_time[i], DateTime.Parse(start)) > 0 && 
                                           DateTime.Compare(_time[i], DateTime.Parse(end)) < 0).ToList();

    _pullForceDailyMax = Math.Round(pullForceList.Max(), 2, MidpointRounding.AwayFromZero);

    return _pullForceDailyMax;
}

이제 ReSharper 가 변경을 제안하는 내용에 대한 설명을 추가했습니다 . 무엇을 의미합니까, 왜 변경해야합니까?implicitly captured closure: end, start


6
MyCodeSucks는 허용 된 답변을 수정하십시오 : kevingessner의 답변은 (설명에 설명 된 바와 같이) 잘못되었으며 허용으로 표시하면 콘솔의 답변을 알지 못하면 사용자를 오도합니다.
Albireo

1
try / catch 외부에서 목록을 정의하고 try / catch에서 모든 추가를 수행 한 다음 결과를 다른 객체로 설정하면이 정보가 표시 될 수도 있습니다. try / catch 내에서 정의 / 추가를 이동하면 GC가 허용됩니다. 잘만되면 이것이 의미가 있습니다.
Micah Montoya

답변:


391

경고는 이 메소드 내부의 람다가 살아남 을 때 변수 endstart유지하고 살아 있음을 알려줍니다 .

간단한 예를 살펴보십시오

protected override void OnLoad(EventArgs e)
{
    base.OnLoad(e);

    int i = 0;
    Random g = new Random();
    this.button1.Click += (sender, args) => this.label1.Text = i++.ToString();
    this.button2.Click += (sender, args) => this.label1.Text = (g.Next() + i).ToString();
}

첫 번째 람다에서 "암시 적으로 캡처 된 클로저 : g"경고가 표시됩니다. 첫 번째 람다가 사용되는 한 가비지 수집이g 불가능 하다는 것을 알려줍니다 .

컴파일러는 람다 식에 대한 클래스를 생성하고 람다 식에 사용되는 모든 변수를 해당 클래스에 넣습니다.

내 예에 따라서 g그리고 i내 대표의 실행을 위해 동일한 클래스에서 개최된다. 경우 g자원의 많은 무거운 개체가 남아있다 긴 람다 표현식의 사용에서와 같이이 클래스에서 참조가 아직 살아 있기 때문에, 가비지 컬렉터는 그것을 회수 할 수 없었다. 따라서 이것은 잠재적 인 메모리 누수이므로 R # 경고의 이유입니다.

@splintor C #에서와 같이 익명 메소드는 항상 메소드 당 하나의 클래스에 저장됩니다. 이것을 피하는 두 가지 방법이 있습니다.

  1. 익명 메소드 대신 인스턴스 메소드를 사용하십시오.

  2. 람다 식 생성을 두 가지 방법으로 나눕니다.


30
이 캡처를 피할 수있는 방법은 무엇입니까?
splintor

2
이 훌륭한 답변에 감사드립니다-한 곳에서 사용하더라도 익명이 아닌 방법을 사용해야하는 이유가 있다는 것을 알게되었습니다.
ScottRhee

1
@splintor 대리자 내부의 개체를 인스턴스화하거나 대신 매개 변수로 전달합니다. 위의 경우 내가 알 수있는 한 원하는 동작은 실제로 Random인스턴스에 대한 참조를 보유하는 것 입니다.
Casey

2
@emodendroket이 시점에서 우리는 코드 스타일과 가독성을 이야기하고 있습니다. 필드는 추론하기가 더 쉽습니다. 메모리 압력이나 객체 수명이 중요한 경우 필드를 선택하고 그렇지 않은 경우 더 간결한 폐쇄 상태로 둡니다.
yzorg 's December

1
필자의 사례는 Foo와 Bar를 만드는 팩토리 메소드로 단순화되었습니다. 그런 다음 두 객체에 노출 된 이벤트에 람 바스 캡처를 구독하고 놀랍게도 Foo는 Bar 이벤트의 람바에서 캡처 한 정보를 유지하고 그 반대도 마찬가지입니다. 나는이 접근법이 잘 작동했을 C ++에서 왔으며 규칙이 여기에서 다르다는 사실에 놀랐습니다. 더 많이 알수록 나는 추측합니다.
dlf

35

Peter Mortensen과 동의 함.

C # 컴파일러는 메소드의 모든 람다 식에 대한 모든 변수를 캡슐화하는 하나의 유형 만 생성합니다.

예를 들어 소스 코드가 다음과 같습니다.

public class ValueStore
{
    public Object GetValue()
    {
        return 1;
    }

    public void SetValue(Object obj)
    {
    }
}

public class ImplicitCaptureClosure
{
    public void Captured()
    {
        var x = new object();

        ValueStore store = new ValueStore();
        Action action = () => store.SetValue(x);
        Func<Object> f = () => store.GetValue();    //Implicitly capture closure: x
    }
}

컴파일러는 다음과 같은 유형을 생성합니다.

[CompilerGenerated]
private sealed class c__DisplayClass2
{
  public object x;
  public ValueStore store;

  public c__DisplayClass2()
  {
    base.ctor();
  }

  //Represents the first lambda expression: () => store.SetValue(x)
  public void Capturedb__0()
  {
    this.store.SetValue(this.x);
  }

  //Represents the second lambda expression: () => store.GetValue()
  public object Capturedb__1()
  {
    return this.store.GetValue();
  }
}

그리고 Capture방법은 다음과 같이 컴파일됩니다.

public void Captured()
{
  ImplicitCaptureClosure.c__DisplayClass2 cDisplayClass2 = new ImplicitCaptureClosure.c__DisplayClass2();
  cDisplayClass2.x = new object();
  cDisplayClass2.store = new ValueStore();
  Action action = new Action((object) cDisplayClass2, __methodptr(Capturedb__0));
  Func<object> func = new Func<object>((object) cDisplayClass2, __methodptr(Capturedb__1));
}

두 번째 람다는을 사용하지 않지만 람다에서 사용 된 생성 된 클래스의 속성으로 컴파일 된 것처럼 x가비지 수집 할 수 없습니다 x.


31

경고는 유효하며 람다둘 이상인 메소드에 표시되며 다른 값캡처합니다 .

람다가 포함 된 메소드가 호출되면 컴파일러 생성 객체는 다음과 같이 인스턴스화됩니다.

  • 람다를 나타내는 인스턴스 메소드
  • 해당 람다 하나에 의해 캡처 된 모든 값을 나타내는 필드

예로서:

class DecompileMe
{
    DecompileMe(Action<Action> callable1, Action<Action> callable2)
    {
        var p1 = 1;
        var p2 = "hello";

        callable1(() => p1++);    // WARNING: Implicitly captured closure: p2

        callable2(() => { p2.ToString(); p1++; });
    }
}

이 클래스에 대해 생성 된 코드를 검사하십시오 (약간 정리 됨).

class DecompileMe
{
    DecompileMe(Action<Action> callable1, Action<Action> callable2)
    {
        var helper = new LambdaHelper();

        helper.p1 = 1;
        helper.p2 = "hello";

        callable1(helper.Lambda1);
        callable2(helper.Lambda2);
    }

    [CompilerGenerated]
    private sealed class LambdaHelper
    {
        public int p1;
        public string p2;

        public void Lambda1() { ++p1; }

        public void Lambda2() { p2.ToString(); ++p1; }
    }
}

LambdaHelper작성된 상점 의 인스턴스 p1와 모두를 참고하십시오 p2.

상상 해봐:

  • callable1 논쟁에 대한 오랜 참조를 유지한다. helper.Lambda1
  • callable2 논증에 대한 언급을 유지하지 않고 helper.Lambda2

이 상황에서에 대한 helper.Lambda1참조는의 문자열을 간접적으로 참조 p2하므로 가비지 수집기가 해당 문자열 을 할당 해제 할 수 없습니다. 최악의 경우 메모리 / 자원 누출입니다. 또는 객체를 필요 이상으로 오래 유지할 수 있습니다. 이는 gen0에서 gen1로 승격 될 경우 GC에 영향을 줄 수 있습니다.


우리의 기준을했다 경우 p1에서 callable2이 같은 : callable2(() => { p2.ToString(); });-이 여전히 동일한 문제 (가비지 컬렉터가 할당을 해제 할 수 없습니다)이 발생하지 것이다 LambdaHelper여전히 포함 p1하고를 p2?
Antony

1
예, 같은 문제가 존재합니다. 컴파일러 LambdaHelper는 상위 메소드 내의 모든 람다에 대해 하나의 캡처 오브젝트 (즉 위)를 작성합니다. 그래서 경우에도 callable2전혀 사용을하지 p1, 그것은 같은 캡처 객체를 공유하는 것 callable1, 그리고 캡처 개체는 모두 참조 할 것 p1하고 p2. 이것은 실제로 참조 유형에만 중요하며이 p1예제에서는 값 유형입니다.
Drew Noakes

3

Linq to Sql 쿼리의 경우이 경고가 표시 될 수 있습니다. 람다의 범위는 메서드가 범위를 벗어난 후에 쿼리가 종종 실현되기 때문에 메서드보다 수명이 길 수 있습니다. 상황에 따라 L2S 람다에서 캡처 된 메소드의 인스턴스 변수에서 GC를 허용하기 위해 메소드 내에서 결과를 실현 (예 : .ToList ()를 통해) 할 수 있습니다.


2

아래에 표시된 힌트를 클릭하여 R # 제안의 이유를 항상 파악할 수 있습니다.

여기에 이미지 설명을 입력하십시오

이 힌트는 당신을 여기로 안내 할 입니다.


이 검사를 통해 눈에 띄게 보이는 것보다 더 많은 폐쇄 값이 포착되고 있다는 사실에주의를 기울여야합니다. 이는 이러한 값의 수명에 영향을 미칩니다.

다음 코드를 고려하십시오.

using System; 
public class Class1 {
    private Action _someAction;

    public void Method() {
        var obj1 = new object();
        var obj2 = new object();

        _someAction += () => {
            Console.WriteLine(obj1);
            Console.WriteLine(obj2);
        };

        // "Implicitly captured closure: obj2"
        _someAction += () => {
            Console.WriteLine(obj1);
        };
    }
}

첫 번째 클로저에서 obj1과 obj2가 명시 적으로 캡처되고 있음을 알 수 있습니다. 코드를 보면 알 수 있습니다. 두 번째 클로저에서는 obj1이 명시 적으로 캡처되고 있음을 알 수 있지만 ReSharper는 obj2가 암시 적으로 캡처되고 있음을 경고합니다.

이것은 C # 컴파일러의 구현 세부 사항 때문입니다. 컴파일 중에 클로저는 캡처 된 값을 보유하는 필드와 클로저 자체를 나타내는 메소드가있는 클래스로 다시 작성됩니다. C # 컴파일러는 메소드 당 하나의 개인 클래스 만 작성하며 메소드에 둘 이상의 클로저가 정의 된 경우이 클래스에는 각 클로저마다 하나씩 여러 메소드가 포함되며 모든 클로저에서 캡처 된 모든 값도 포함됩니다.

컴파일러가 생성하는 코드를 살펴보면 다음과 같이 보입니다 (일부 이름은 정리하기 쉽도록 정리되었습니다).

public class Class1 {
    [CompilerGenerated]
    private sealed class <>c__DisplayClass1_0
    {
        public object obj1;
        public object obj2;

        internal void <Method>b__0()
        {
            Console.WriteLine(obj1);
            Console.WriteLine(obj2);
        }

        internal void <Method>b__1()
        {
            Console.WriteLine(obj1);
        }
    }

    private Action _someAction;

    public void Method()
    {
        // Create the display class - just one class for both closures
        var dc = new Class1.<>c__DisplayClass1_0();

        // Capture the closure values as fields on the display class
        dc.obj1 = new object();
        dc.obj2 = new object();

        // Add the display class methods as closure values
        _someAction += new Action(dc.<Method>b__0);
        _someAction += new Action(dc.<Method>b__1);
    }
}

메소드가 실행될 때 모든 클로저에 대한 모든 값을 캡처하는 표시 클래스를 작성합니다. 따라서 클로저 중 하나에서 값을 사용하지 않더라도 여전히 캡처됩니다. 이것은 ReSharper가 강조하고있는 "암시 적"캡처입니다.

이 검사의 의미는 암시 적으로 캡처 된 클로저 값이 클로저 자체가 가비지 수집 될 때까지 가비지 수집되지 않는다는 것입니다. 이 값의 수명은 이제 값을 명시 적으로 사용하지 않는 클로저의 수명과 연결됩니다. 클로저가 오래 지속되는 경우, 특히 캡처 된 값이 매우 큰 경우 코드에 부정적인 영향을 줄 수 있습니다.

이것은 컴파일러의 구현 세부 사항이지만 Microsoft (Roslyn 이전 및 이후) ​​또는 Mono의 컴파일러와 같은 버전과 구현에서 일관성이 있습니다. 값 유형을 캡처하는 여러 클로저를 올바르게 처리하려면 구현이 설명 된대로 작동해야합니다. 예를 들어, 여러 클로저가 int를 캡처하는 경우 동일한 인스턴스를 캡처해야합니다.이 인스턴스는 단일 공유 개인 중첩 클래스에서만 발생할 수 있습니다. 이것의 부작용은 모든 캡처 된 값의 수명이 이제 모든 값을 캡처하는 모든 클로저의 최대 수명이라는 것입니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.