Що таке "закриття" в .NET?


195

Що таке закриття ? Чи є вони у них .NET?

Якщо вони існують у .NET, ви можете надати фрагмент коду (бажано в C #), пояснивши його?

Відповіді:


258

У мене є стаття на цю саму тему . (Має багато прикладів.)

По суті, закриття - це блок коду, який може бути виконаний пізніше, але який підтримує середовище, в якому він був створений вперше - тобто він все ще може використовувати локальні змінні тощо методу, який його створив, навіть після цього метод завершив виконання.

Загальна особливість закриття реалізована в C # анонімними методами та лямбда-виразами.

Ось приклад використання анонімного методу:

using System;

class Test
{
    static void Main()
    {
        Action action = CreateAction();
        action();
        action();
    }

    static Action CreateAction()
    {
        int counter = 0;
        return delegate
        {
            // Yes, it could be done in one statement; 
            // but it is clearer like this.
            counter++;
            Console.WriteLine("counter={0}", counter);
        };
    }
}

Вихід:

counter=1
counter=2

Тут ми можемо побачити, що дія, повернена CreateAction, все ще має доступ до змінної лічильника, і може дійсно збільшувати її, навіть незважаючи на те, що саме CreateAction завершився.


57
Дякую, Джон. До речі, є щось, чого ви не знаєте в .NET? :) До кого ви їдете, коли у вас є питання?
розробник

44
Завжди можна дізнатися більше :) Я щойно закінчив читати CLR через C # - дуже інформативно. Крім цього, я зазвичай прошу Марка Гравелла для дерев WCF / прив'язки / вираження, а Еріка Ліпперта - для мов C #.
Джон Скіт

2
Я помітив це, але я все-таки вважаю, що ваше твердження про те, що він є "блоком коду, який можна виконати в більш пізній час", просто невірно - це не має нічого спільного з виконанням, більше стосується змінних значень і сфери, ніж виконання , як така.
Джейсон Бантінг

11
Я б сказав, що закриття не є корисним, якщо вони не можуть бути виконані, а "пізніше" підкреслює "дивацтво" можливості захоплення оточення (що в іншому випадку могло б піти з часу виконання). Якщо ви цитуєте лише половину речення, то, звичайно, це неповна відповідь.
Джон Скіт

4
@SLC: Так, counterможна збільшувати - компілятор генерує клас, який містить counterполе, і будь-який код, що посилається на counterкінець, проходить через екземпляр цього класу.
Джон Скіт

22

Якщо вам цікаво побачити, як C # реалізує закриття, прочитане "Я знаю відповідь (його 42) блог"

Компілятор генерує клас у фоновому режимі, щоб інкапсулювати анонімний метод і змінну j

[CompilerGenerated]
private sealed class <>c__DisplayClass2
{
    public <>c__DisplayClass2();
    public void <fillFunc>b__0()
    {
       Console.Write("{0} ", this.j);
    }
    public int j;
}

для функції:

static void fillFunc(int count) {
    for (int i = 0; i < count; i++)
    {
        int j = i;
        funcArr[i] = delegate()
                     {
                         Console.Write("{0} ", j);
                     };
    } 
}

Перетворення:

private static void fillFunc(int count)
{
    for (int i = 0; i < count; i++)
    {
        Program.<>c__DisplayClass1 class1 = new Program.<>c__DisplayClass1();
        class1.j = i;
        Program.funcArr[i] = new Func(class1.<fillFunc>b__0);
    }
}

Привіт, Даніеле - Ти відповідь дуже корисна, і я хотів вийти за рамки вашої відповіді та продовжити, але посилання порушено. На жаль, мій googlefu недостатньо хороший, щоб знайти, куди він переїхав.
Нокс

11

Замикання - це функціональні значення, які містять змінні значення від їх початкової області. C # може використовувати їх у вигляді анонімних делегатів.

Для дуже простого прикладу візьміть цей C # код:

    delegate int testDel();

    static void Main(string[] args)
    {
        int foo = 4;
        testDel myClosure = delegate()
        {
            return foo;
        };
        int bar = myClosure();

    }

В кінці цього рядка буде встановлено значення 4, і делегат myClosure може бути переданий навколо, щоб його використовувати в інших місцях програми.

Закриття може використовуватися для багатьох корисних речей, таких як затримка виконання або для спрощення інтерфейсів - LINQ в основному будується за допомогою закриття. Найбільш безпосереднім способом, який це стане в нагоді для більшості розробників, є додавання обробників подій до динамічно створених елементів керування - ви можете використовувати закриття, щоб додати поведінку, коли елемент керування створений, а не зберігати дані в іншому місці.


10
Func<int, int> GetMultiplier(int a)
{
     return delegate(int b) { return a * b; } ;
}
//...
var fn2 = GetMultiplier(2);
var fn3 = GetMultiplier(3);
Console.WriteLine(fn2(2));  //outputs 4
Console.WriteLine(fn2(3));  //outputs 6
Console.WriteLine(fn3(2));  //outputs 6
Console.WriteLine(fn3(3));  //outputs 9

Закриття - анонімна функція, передана поза функцією, в якій вона створена. Він підтримує будь-які змінні з функції, в якій він створений, який він використовує.


4

Ось надуманий приклад для C #, який я створив із подібного коду в JavaScript:

public delegate T Iterator<T>() where T : class;

public Iterator<T> CreateIterator<T>(IList<T> x) where T : class
{
        var i = 0; 
        return delegate { return (i < x.Count) ? x[i++] : null; };
}

Отже, ось якийсь код, який показує, як використовувати вищевказаний код ...

var iterator = CreateIterator(new string[3] { "Foo", "Bar", "Baz"});

// So, although CreateIterator() has been called and returned, the variable 
// "i" within CreateIterator() will live on because of a closure created 
// within that method, so that every time the anonymous delegate returned 
// from it is called (by calling iterator()) it's value will increment.

string currentString;    
currentString = iterator(); // currentString is now "Foo"
currentString = iterator(); // currentString is now "Bar"
currentString = iterator(); // currentString is now "Baz"
currentString = iterator(); // currentString is now null

Сподіваюся, що це дещо корисно.


1
Ви подали приклад, але не запропонували загального визначення. З ваших коментарів я добираюсь, що вони "більше стосуються сфери", але, безумовно, є в цьому більше?
ladenedge

2

Закриття - це шматки коду, які посилаються на змінну поза собою (з-під них на стеку), яку можна викликати або виконати пізніше (наприклад, коли визначено подію або делегата, і їх можна буде викликати в якийсь невизначений майбутній момент часу ) ... Оскільки зовнішня змінна, що фрагмент посилань коду може вийти за межі (і в іншому випадку була б втрачена), той факт, що на неї посилається фрагмент коду (називається закриттям), говорить час виконання "утримувати "ця змінна сфера дії, поки вона більше не потрібна фрагменту коду закриття ...


Як я зазначив в чужому поясненні: я ненавиджу бути технічним, але закриття має більше спільного з масштабом - закриття може бути створене двома різними способами, але закриття не є засобом, це кінець.
Джейсон Бантінг

1
Закриття для мене відносно нове, тому цілком можливо, що я неправильно розумію, але я отримую частину обсягу. Моя відповідь зосереджена навколо сфери застосування. Тож я пропускаю те, що ваш коментар намагається виправити ... Що ще може стосуватися сфери коду, окрім фрагмента коду? (функція, анонімний метод чи що завгодно)
Чарльз Бретана

Чи не є ключем до закриття того, що якийсь "шматок виконуваного коду" може отримати доступ до змінної чи значення в пам'яті, яке воно синтаксично знаходиться "поза" сфери його дії, після того, як ця змінна повинна звичайно вийти "за межі" або бути знищена ?
Чарльз Бретана

І @ Джейсон, не хвилюйтеся з технічної точки зору, ця ідея закриття - це те, що мені знадобилося деякий час, щоб загорнути голову, в довгих дискусіях з колегою, про закриття javascript ... але він був гайкою Ліспа, і я ніколи цілком пережив абстракції в своїх поясненнях ...
Чарльз Бретана

2

В основному закриття - це блок коду, який ви можете передати як аргумент функції. C # підтримує закриття у вигляді анонімних делегатів.

Ось простий приклад:
Метод List.Find може прийняти та виконати фрагмент коду (закриття), щоб знайти елемент списку.

// Passing a block of code as a function argument
List<int> ints = new List<int> {1, 2, 3};
ints.Find(delegate(int value) { return value == 1; });

Використовуючи синтаксис C # 3.0, ми можемо записати це як:

ints.Find(value => value == 1);

1
Я ненавиджу бути технічним, але закриття має більше стосунку до сфери застосування - закриття може бути створене двома різними способами, але закриття не є засобом, це кінець.
Джейсон Бантінг

2

Закриття - це коли функція визначена всередині іншої функції (або методу), і вона використовує змінні з батьківського методу . Таке використання змінних, які знаходяться у методі та утворені у визначеній всередині нього функції, називається закриттям.

У своєму дописі на блозі Марк Семанн має кілька цікавих прикладів закриття де він робить паралель між oop та функціональним програмуванням.

І щоб зробити це більш детальним

var workingDirectory = new DirectoryInfo(Environment.CurrentDirectory);//when this variable
Func<int, string> read = id =>
    {
        var path = Path.Combine(workingDirectory.FullName, id + ".txt");//is used inside this function
        return File.ReadAllText(path);
    };//the entire process is called a closure.

0

Я також намагався це зрозуміти. Нижче наведено фрагменти коду для того самого коду в Javascript та C # із зазначенням закриття.

  1. Відраховуйте кількість разів, коли кожна подія траплялася або не було натискано кожної кнопки.

JavaScript:

var c = function ()
{
    var d = 0;

    function inner() {
      d++;
      alert(d);
  }

  return inner;
};

var a = c();
var b = c();

<body>
<input type=button value=call onClick="a()"/>
  <input type=button value=call onClick="b()"/>
</body>

C #:

using System.IO;
using System;

class Program
{
    static void Main()
    {
      var a = new a();
      var b = new a();

       a.call();
       a.call();
       a.call();

       b.call();
       b.call();
       b.call();
    }
}

public class a {

    int b = 0;

    public  void call()
    {
      b++;
     Console.WriteLine(b);
    }
}
  1. підрахунок Загальна кількість разів, коли траплялася подія клацання, або загальна кількість кліків, незалежно від контролю.

JavaScript:

var c = function ()
{
    var d = 0;

    function inner() {
     d++;
     alert(d);
  }

  return inner;
};

var a = c();

<input type=button value=call onClick="a()"/>
  <input type=button value=call onClick="a()"/>

C #:

using System.IO;
using System;

class Program
{
    static void Main()
    {
      var a = new a();
      var b = new a();

       a.call();
       a.call();
       a.call();

       b.call();
       b.call();
       b.call();
    }
}

public class a {

    static int b = 0;

    public void call()
    {
      b++;
     Console.WriteLine(b);
    }
}

0

Нещодавно, простий і більш зрозумілий відповідь із книги "С # 7.0".

Попередній запит, який ви повинні знати : Вираз лямбда може посилатися на локальні змінні та параметри методу, в якому він визначений (зовнішні змінні).

    static void Main()
    {
    int factor = 2;
   //Here factor is the variable that takes part in lambda expression.
    Func<int, int> multiplier = n => n * factor;
    Console.WriteLine (multiplier (3)); // 6
    }

Справжня частина : Зовнішні змінні, на які посилається лямбда-вираз, називаються захопленими змінними. Лямбда-вираз, який фіксує змінні, називається закриттям.

Остання точка, яку слід зазначити : Захоплені змінні оцінюються тоді, коли фактично викликається делегат, а не коли були змінені змінні:

int factor = 2;
Func<int, int> multiplier = n => n * factor;
factor = 10;
Console.WriteLine (multiplier (3)); // 30

0

Якщо ви пишете вбудований анонімний метод (C # 2) або (бажано) вираз лямбда (C # 3 +), фактичний метод все ще створюється. Якщо цей код використовує локальну змінну зовнішньої області - вам все одно потрібно якось передати цю змінну методу.

наприклад, прийняти цей пункт Linq Where (це простий метод розширення, який передає лямбда-вираз):

var i = 0;
var items = new List<string>
{
    "Hello","World"
};   
var filtered = items.Where(x =>
// this is a predicate, i.e. a Func<T, bool> written as a lambda expression
// which is still a method actually being created for you in compile time 
{
    i++;
    return true;
});

якщо ви хочете використовувати i в цьому лямбда-виразі, ви повинні передати його створеному методу.

Тож перше питання, що виникає, це: чи слід його передавати за значенням чи посиланням?

Перехід за посиланням є (я думаю) більш кращим, оскільки ви отримуєте доступ до читання / запису до цієї змінної (і це те, що робить C #; я думаю, команда в Microsoft зважила плюси і мінуси і пішла з посиланням; За словами Джона Скіта стаття , Java пішла з вартістю).

Але тоді виникає ще одне питання: куди це виділити?

Чи слід насправді / природно виділяти на стек? Ну, якщо ви виділите його на стек і передасте його за посиланням, можуть виникнути ситуації, коли це переживає його власний фрейм стека. Візьмемо цей приклад:

static void Main(string[] args)
{
    Outlive();
    var list = whereItems.ToList();
    Console.ReadLine();
}

static IEnumerable<string> whereItems;

static void Outlive()
{
    var i = 0;
    var items = new List<string>
    {
        "Hello","World"
    };            
    whereItems = items.Where(x =>
    {
        i++;
        Console.WriteLine(i);
        return true;
    });            
}

Вираз лямбда (у пункті Where) знову створює метод, що посилається на i. Якщо я виділяється на стек Outlive, то до моменту перерахування whereItems, i, що використовується в створеному методі, буде вказувати на i з Outlive, тобто на місце в стеку, яке вже недоступне.

Гаразд, значить, тоді нам це потрібно на купі.

Отже, що компілятор C # робить для підтримки цього вбудованого анонімного / лямбда, це використовувати те, що називається " Closures ": Це створює клас на Heap під назвою ( досить погано ) DisplayClass, який має поле, що містить i, і функцію, яка фактично використовує це.

Щось, що було б еквівалентно цьому (ви можете бачити ІЛ, згенерований за допомогою ILSpy або ILDASM):

class <>c_DisplayClass1
{
    public int i;

    public bool <GetFunc>b__0()
    {
        this.i++;
        Console.WriteLine(i);
        return true;
    }
}

Він створює цей клас у вашому локальному масштабі та замінює будь-який код, що стосується i або вираз лямбда, цим екземпляром закриття. Отже, щоразу, коли ви використовуєте i у коді "локальної області", де я був визначений, ви фактично використовуєте це поле екземпляра DisplayClass.

Отже, якщо я змінив би "локальний" i в основному методі, він фактично змінить _DisplayClass.i;

тобто

var i = 0;
var items = new List<string>
{
    "Hello","World"
};  
var filtered = items.Where(x =>
{
    i++;
    return true;
});
filtered.ToList(); // will enumerate filtered, i = 2
i = 10;            // i will be overwriten with 10
filtered.ToList(); // will enumerate filtered again, i = 12
Console.WriteLine(i); // should print out 12

він надрукує 12, оскільки "i = 10" переходить до цього поля диспалікласу та змінює його перед 2-м перерахуванням.

Хорошим джерелом теми є цей модуль Pluralsight Bart De Smet (вимагає реєстрації) (також ігноруйте його помилкове використання терміна "Піднімання" - що (я думаю) він означає, що локальна змінна (тобто я) змінена для позначення до нового поля DisplayClass).


В інших новинах, мабуть, існує помилкове уявлення про те, що "Закриття" пов'язане з циклами - наскільки я розумію, "Закриття" НЕ є поняттям, пов’язаним із циклами , а скоріше з анонімними методами / лямбда-виразами, що використовують локальні змінні масштабування - хоча якийсь трюк Питання використовують петлі, щоб продемонструвати це.


-1

Закриття - це функція, визначена у функції, яка може отримати доступ до локальних змінних її, а також до її батьківського.

public string GetByName(string name)
{
   List<things> theThings = new List<things>();
  return  theThings.Find<things>(t => t.Name == name)[0];
}

тому функція всередині методу пошуку.

 t => t.Name == name

може отримати доступ до змінних всередині своєї області дії, t та імені змінної, що знаходиться в області її батьків. Незважаючи на те, що він виконується методом find як делегат, з іншої області всі разом.


2
Закриття не є функцією, сама по собі, вона більше визначається, якщо говорити про область, ніж про функції. Функції просто допомагають утримувати рамки навколо, що спричиняє створення закриття. Але сказати, що закриття - це функція, технічно не вірна. Вибачте, нітпік. :)
Джейсон Бантінг
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.