Подтвердить что ты не робот

С# Lambdas и переменная "this"

Мне интересно, могу ли я использовать ключевое слово this внутри С# лямбда, хотя на самом деле я знаю, что могу, но я хочу, чтобы убедиться, что это не так, или позже вы получите тонкие проблемы.

Прочитав правила область переменных для lambdas, я вижу, что:

Перехваченная переменная не будет собираться мусором до тех пор, пока делегат, который ссылается на него, выходит за рамки.

Итак, это заставляет меня предположить, что экземпляр объекта (this) также будет захвачен. Чтобы проверить это, я написал этот надуманный пример, к которому я хочу приблизиться в своем реальном коде, написанном в LINQPad, поэтому почему у меня есть метод Dump():

void Main()
{
    Repository repo = new Repository();
    Person person = repo.GetPerson(1);

    person.ID.Dump("Person ID - Value Assigned");
    person.Name.Dump("Person Name - Lazily Created");
}

class Person
{
    public Person(Lazy<string> name)
    {
        this.name = name;
    }

    public int ID { get; set; }

    private Lazy<string> name;
    public string Name
    {
        get { return name.Value; }
    }
}

class Repository
{
    public Person GetPerson(int id)
    {
        // Setup person to lazily load a name value
        Person person = new Person(
            new Lazy<string>(
                () => this.GetName()    // <--- This I'm not sure on...
            )
        );
        person.ID = id;
        return person;
    }

    public string GetName()
    {
        return "John Smith";
    }
}

Это выполняется и дает мне правильный результат, поэтому доступ к this изнутри лямбда явно работает. Однако я хочу проверить:

  • Выполняет ли это те же правила области видимости переменных, что и локальные переменные, что означает, что ссылка this хранится в памяти до тех пор, пока лямбда больше не будет использоваться? Это было бы так из моего небольшого эксперимента, но если кто-то может дать более подробную информацию, мне было бы интересно.
  • Это целесообразно? Я не хочу вдаваться в ситуацию позже, когда этот шаблон может вызвать проблемы.
4b9b3361

Ответ 1

Нет ничего плохого в использовании this в лямбда, но, как вы говорите, если вы используете this (или если вы используете его неявно, вызывая любую нестатистическую функцию-член или используя нестатические переменные-члены), тогда сборщик мусора будет хранить объект, который this относится к живым, по крайней мере, до тех пор, пока делегат жив. Поскольку вы передаете лямбда на Lazy, это означает, что Repository будет жив как минимум до тех пор, пока объект Lazy жив (даже если вы никогда не звоните Lazy.Value).

Чтобы немного ослабить его, он помогает разобраться в дизассемблере. Рассмотрим этот код:

class Foo {
    static Action fLambda, gLambda;

    int x;
    void f() {
        int y = 0;
        fLambda = () => ++y;
    }
    void g() {
        int y = 0;
        gLambda = () => y += x;
    }
}

Стандартный компилятор изменит это на следующее (попробуйте игнорировать дополнительные угловые скобки <>). Как вы можете видеть, лямбды, которые используют переменные изнутри тела функции, преобразуются в классы:

internal class Foo
{
    private static Action fLambda;
    private static Action gLambda;
    private int x;

    private void f()
    {
        Foo.<>c__DisplayClass1 <>c__DisplayClass = new Foo.<>c__DisplayClass1();
        <>c__DisplayClass.y = 0;
        Foo.fLambda = new Action(<>c__DisplayClass.<f>b__0);
    }
    private void g()
    {
        Foo.<>c__DisplayClass4 <>c__DisplayClass = new Foo.<>c__DisplayClass4();
        <>c__DisplayClass.<>4__this = this;
        <>c__DisplayClass.y = 0;
        Foo.gLambda = new Action(<>c__DisplayClass.<g>b__3);
    }

    [CompilerGenerated]
    private sealed class <>c__DisplayClass1
    {
        public int y;
        public void <f>b__0()
        {
            this.y++;
        }
    }
    [CompilerGenerated]
    private sealed class <>c__DisplayClass4
    {
        public int y;
        public Foo <>4__this;
        public void <g>b__3()
        {
            this.y += this.<>4__this.x;
        }
    }

}

Если вы используете this, неявно или явно, он становится переменной-членом в классе, генерируемом компилятором. Таким образом, класс для f(), DisplayClass1 не содержит ссылки на Foo, но класс для g(), DisplayClass2, делает.

Компилятор обрабатывает lambdas более простым способом, если они не ссылаются на какие-либо локальные переменные. Поэтому рассмотрим несколько немного отличающийся код:

public class Foo {
    static Action pLambda, qLambda;

    int x;
    void p() {
        int y = 0;
        pLambda = () => Console.WriteLine("Simple lambda!");
    }
    void q() {
        int y = 0;
        qLambda = () => Console.WriteLine(x);
    }
}

В этот раз lambdas не ссылаются на какие-либо локальные переменные, поэтому компилятор переводит ваши лямбда-функции в обычные функции. Лямбда в p() не использует this, поэтому она становится статической функцией (называемой <p>b__0); lambda в q() использует this (неявно), поэтому он становится нестатической функцией (называемой <q>b__2):

public class Foo {
    private static Action pLambda, qLambda;

    private int x;
    private void p()
    {
        Foo.pLambda = new Action(Foo.<p>b__0);
    }
    private void q()
    {
        Foo.qLambda = new Action(this.<q>b__2);
    }
    [CompilerGenerated] private static void <p>b__0()
    {
        Console.WriteLine("Simple lambda!");
    }
    [CompilerGenerated] private void <q>b__2()
    {
        Console.WriteLine(this.x);
    }
    // (I don't know why this is here)
    [CompilerGenerated] private static Action CS$<>9__CachedAnonymousMethodDelegate1;
}

Примечание. Я просмотрел вывод компилятора с помощью ILSpy с опцией "декомпилировать анонимные методы /lambdas " выключено.

Ответ 2

Хотя правильно использовать this в такой лямбда, вам просто нужно знать, что ваш объект Repository не будет собирать мусор, пока ваш объект Person не станет сборщиком мусора.

Возможно, вам понадобится поле для кэширования результата из вашей лямбда, и после того, как он будет заполнен Lazy, отпустите лямбда, так как вам это больше не нужно.

Что-то вроде:

private Lazy<string> nameProxy; 
private string name;
public string Name 
{ 
  get 
  {
    if(name==null)
    {
      name = nameProxy.Value;
      nameProxy = null;
    }
    return name;
  } 
} 

Ответ 3

Абсолютно отлично использовать this в lambdas, но есть некоторые вещи, которые вы должны иметь в виду:

  • this будет храниться в памяти до тех пор, пока лямбда больше не будет использоваться
  • Если вы не проходите лямбда "с this" вне своего класса, тогда вы не столкнетесь с проблемами.
  • если вы проходите лямбда "с this" вне вашего класса, то вы должны помнить, что ваш класс не будет собран GC, пока не появятся ссылки на лямбда слева.

И связавшись с вашим прецедентом, вы должны иметь в виду, что экземпляр Repository никогда не будет собран GC до тех пор, пока люди, созданные им, не будут использованы.