다음 코드가 있습니다.
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
답변
경고는 이 메소드 내부의 람다가 살아남 을 때 변수 end
를 start
유지하고 살아 있음을 알려줍니다 .
간단한 예를 살펴보십시오
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 #에서와 같이 익명 메소드는 항상 메소드 당 하나의 클래스에 저장됩니다. 이것을 피하는 두 가지 방법이 있습니다.
-
익명 메소드 대신 인스턴스 메소드를 사용하십시오.
-
람다 식 생성을 두 가지 방법으로 나눕니다.
답변
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
.
답변
경고는 유효하며 람다 가 둘 이상인 메소드에 표시되며 다른 값 을 캡처합니다 .
람다가 포함 된 메소드가 호출되면 컴파일러 생성 객체는 다음과 같이 인스턴스화됩니다.
- 람다를 나타내는 인스턴스 메소드
- 해당 람다 중 하나에 의해 캡처 된 모든 값을 나타내는 필드
예로서:
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에 영향을 줄 수 있습니다.
답변
Linq to Sql 쿼리의 경우이 경고가 표시 될 수 있습니다. 람다의 범위는 메서드가 범위를 벗어난 후에 쿼리가 종종 실현되기 때문에 메서드보다 수명이 길 수 있습니다. 상황에 따라 L2S 람다에서 캡처 된 메소드의 인스턴스 변수에서 GC를 허용하기 위해 메소드 내에서 결과를 실현 (예 : .ToList ()를 통해) 할 수 있습니다.
답변
아래에 표시된 힌트를 클릭하여 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를 캡처하는 경우 동일한 인스턴스를 캡처해야합니다.이 인스턴스는 단일 공유 개인 중첩 클래스에서만 발생할 수 있습니다. 이것의 부작용은 모든 캡처 된 값의 수명이 이제 모든 값을 캡처하는 모든 클로저의 최대 수명이라는 것입니다.