'การปิด' ใน. 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
ขอบคุณจอน BTW มีบางอย่างที่คุณไม่รู้ใน. NET หรือไม่? :) คุณจะไปใครเมื่อคุณมีคำถาม?
นักพัฒนา

44
มีอะไรอีกมากมายให้เรียนรู้ :) ฉันเพิ่งอ่าน CLR ผ่าน C # เสร็จแล้ว - มีข้อมูลมาก นอกเหนือจากนั้นฉันมักจะถาม Marc Gravell สำหรับต้นไม้ WCF / binding / expression และ Eric Lippert สำหรับภาษา C #
Jon Skeet

2
ฉันสังเกตเห็นว่า แต่ฉันยังคงคิดว่าคำสั่งของคุณเกี่ยวกับการเป็น "บล็อกของรหัสที่สามารถดำเนินการได้ในภายหลัง" นั้นไม่ถูกต้อง - มันไม่มีอะไรเกี่ยวข้องกับการดำเนินการมากกว่าค่าตัวแปรและขอบเขตมากกว่าการดำเนินการ ต่อ
เจสันตอม่อ

11
ฉันจะบอกว่าการปิดไม่มีประโยชน์เว้นแต่พวกเขาจะสามารถดำเนินการได้และ "ในเวลาต่อมา" จะเน้นถึง "ความแปลก" ที่สามารถจับสภาพแวดล้อมได้ (ซึ่งอาจหายไปตามเวลาดำเนินการ) หากคุณพูดเพียงครึ่งประโยคก็เป็นคำตอบที่ไม่สมบูรณ์แน่นอน
Jon Skeet

4
@SLC: ใช่counterสามารถเพิ่มค่าได้ - คอมไพเลอร์สร้างคลาสที่มีcounterฟิลด์และโค้ดใด ๆ ที่อ้างอิงถึงการcounterเข้าสู่อินสแตนซ์ของคลาสนั้น
Jon Skeet

22

หากคุณสนใจที่จะดูว่า C # ดำเนินการปิดการอ่านได้อย่างไร"ฉันรู้คำตอบ (42) บล็อก"

คอมไพเลอร์สร้างคลาสในพื้นหลังเพื่อแค็ปซูลวิธี anoymous และตัวแปร 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);
    }
}

สวัสดี Daniil - คำตอบของคุณมีประโยชน์มากและฉันต้องการที่จะตอบมากกว่าคำตอบของคุณและติดตาม แต่ลิงก์เสีย น่าเสียดายที่ googlefu ของฉันไม่ดีพอที่จะหาที่ที่มันย้ายไป
Knox

11

Closures คือค่าการใช้งานที่เก็บค่าตัวแปรจากขอบเขตดั้งเดิม 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
คุณได้รับตัวอย่าง แต่ไม่ได้ให้คำจำกัดความทั่วไป ฉันรวบรวมจากความคิดเห็นของคุณที่นี่ว่าพวกเขา 'เพิ่มเติมเกี่ยวกับขอบเขต' แต่แน่นอนมีมากกว่านั้น
ภาระ

2

Closures คือโค้ดที่อ้างอิงตัวแปรภายนอกตัวเอง (จากด้านล่างพวกมันบนสแต็ก) ซึ่งอาจถูกเรียกใช้หรือเรียกใช้ในภายหลัง (เช่นเมื่อมีการกำหนดเหตุการณ์หรือผู้ได้รับมอบหมายและสามารถเรียกได้ที่จุดอนาคตไม่แน่นอน ) ... เนื่องจากตัวแปรภายนอกที่กลุ่มของการอ้างอิงรหัสอาจหายไปจากขอบเขต (และอาจสูญหายไป) ความจริงที่ว่ามันถูกอ้างอิงโดยกลุ่มของรหัส (เรียกว่าการปิด) บอกรันไทม์เพื่อ "ถือ "ตัวแปรนั้นอยู่ในขอบเขตจนกว่าจะไม่จำเป็นต้องใช้โดยการปิดโค้ดอีกต่อไป ...


ตามที่ฉันระบุไว้ในคำอธิบายของคนอื่น: ฉันเกลียดที่จะเป็นนักวิชาการ แต่การปิดมีส่วนเกี่ยวข้องกับขอบเขตมากกว่า - การปิดสามารถสร้างได้สองวิธีที่แตกต่างกัน แต่การปิดไม่ใช่วิธีการ
35490 Jason

1
การปิดเป็นเรื่องที่ค่อนข้างใหม่สำหรับฉันดังนั้นจึงเป็นไปได้ทั้งหมดที่ฉันเข้าใจผิด แต่ฉันได้รับส่วนขอบเขต .. คำตอบของฉันมุ่งเน้นไปที่ขอบเขต ดังนั้นฉันจึงขาดความคิดเห็นสิ่งที่ปีพยายามแก้ไข ... ขอบเขตสามารถเกี่ยวข้องกับอะไร แต่บางส่วนของรหัส? (ฟังก์ชั่นวิธีการที่ไม่ระบุชื่อหรืออะไรก็ตาม)
ชาร์ลส์ Bretana

ไม่ใช่กุญแจสำคัญในการปิดที่ "บางอันของโค้ดที่รันได้" สามารถเข้าถึงตัวแปรหรือค่าในหน่วยความจำที่มัน syntactically "นอก" ของขอบเขตมันหลังจากตัวแปรนั้นควรจะหายไป "นอกขอบเขต" หรือถูกทำลาย ?
46460 Charles Bretana

และ @Jason ไม่ต้องกังวลเกี่ยวกับการเป็นช่างเทคนิคความคิดการปิดนี้เป็นสิ่งที่ฉันใช้เวลาสักครู่หนึ่งเพื่อให้หัวของฉันพันไปรอบ ๆ พร้อมพูดคุยกับเพื่อนร่วมงานนานเกี่ยวกับการปิด JavaScript ... แต่เขาเป็นถั่ว Lisp และฉันไม่เคย ค่อนข้างได้ผ่านแนวคิดในคำอธิบายของเขา ...
ชาร์ลส์ Bretana

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
ฉันเกลียดที่จะเป็นช่างเทคนิค แต่การปิดมีส่วนเกี่ยวข้องกับขอบเขตมากกว่า - การปิดสามารถสร้างได้หลายวิธี แต่การปิดไม่ใช่วิธีการ
35490 Jason

2

ปิดคือเมื่อฟังก์ชั่นที่มีการกำหนดภายในฟังก์ชั่นอื่น (หรือวิธีการ) และจะใช้ตัวแปรจากวิธีการปกครอง การใช้ตัวแปรซึ่งตั้งอยู่ในวิธีการและห่อหุ้มในฟังก์ชั่นที่กำหนดไว้ภายในนั้นเรียกว่าการปิด

Mark Seemann มีตัวอย่างที่น่าสนใจของการปิดในโพสต์บล็อกของเขาที่เขาทำ paralel ระหว่าง 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. นับไม่กี่ครั้งในแต่ละเหตุการณ์ที่เกิดขึ้นหรือไม่มีการคลิกแต่ละครั้ง

จาวาสคริปต์:

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>

ค#:

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()"/>

ค#:

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

เพิ่งออกมาจากสีน้ำเงินคำตอบที่เข้าใจง่ายและเข้าใจง่ายกว่าจากหนังสือ C # 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 clause (ซึ่งเป็นวิธีการขยายอย่างง่ายซึ่งผ่านการแสดงออกแลมบ์ดา):

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 ชั่งน้ำหนักข้อดีข้อเสียและไปโดยอ้างอิงอ้างอิงตามJon Skeet ของ บทความ 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 อีกครั้ง หาก i ถูกจัดสรรบนสแต็กของ Outlive จากนั้นตามเวลาที่คุณระบุ whereItems, i ที่ใช้ในวิธีการที่สร้างขึ้นจะชี้ไปที่ i ของ Outlive เช่นไปยังสถานที่ในสแต็กที่ไม่สามารถเข้าถึงได้อีกต่อไป

ตกลงดังนั้นเราต้องการมันในกองแล้ว

ดังนั้นสิ่งที่คอมไพเลอร์ C # ทำเพื่อสนับสนุนอินไลน์นิรนาม / แลมบ์ดานั้นใช้สิ่งที่เรียกว่า "การปิด ": มันสร้างคลาสบนฮีปที่เรียกว่า ( ค่อนข้างต่ำ ) DisplayClass ซึ่งมีฟิลด์ที่มี i และฟังก์ชั่นที่ใช้จริง มัน.

สิ่งที่จะเทียบเท่ากับสิ่งนี้ (คุณสามารถดู IL ที่สร้างขึ้นโดยใช้ ILSpy หรือ ILDASM):

class <>c_DisplayClass1
{
    public int i;

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

เป็นอินสแตนซ์ของคลาสนั้นในขอบเขตของคุณและแทนที่โค้ดใด ๆ ที่เกี่ยวข้องกับ i หรือแลมบ์ดานิพจน์ด้วยอินสแตนซ์การปิดนั้น ดังนั้น - เมื่อใดก็ตามที่คุณใช้ i ในโค้ด "local scope" ที่ฉันกำหนดไว้คุณจะใช้ฟิลด์อินสแตนซ์ DisplayClass นั้น

ดังนั้นถ้าฉันจะเปลี่ยน "ท้องถิ่น" ฉันในวิธีการหลักมันจะเปลี่ยน _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" ไปที่ฟิลด์ dispalyclass และเปลี่ยนก่อนการแจงนับครั้งที่ 2

แหล่งที่ดีในหัวข้อนี้คือโมดูล Pluralsight ของ Bart De Smet (ต้องลงทะเบียน) (เช่นเดียวกับการเพิกเฉยต่อการใช้คำว่า "Hoisting" ที่ผิดพลาด - สิ่งที่ (ฉันคิดว่า) เขาหมายถึงว่าตัวแปรท้องถิ่น (เช่น i) ถูกเปลี่ยนเป็นอ้างอิง ไปที่ฟิลด์ 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 และชื่อตัวแปรที่อยู่ในขอบเขตพาเรนต์ แม้ว่าจะถูกดำเนินการโดยวิธีการค้นหาในฐานะผู้รับมอบสิทธิ์จากขอบเขตอื่นทั้งหมดเข้าด้วยกัน


2
การปิดไม่ได้เป็นฟังก์ชั่นตามที่กำหนดไว้โดยการพูดคุยเกี่ยวกับขอบเขตกว่าฟังก์ชั่น ฟังก์ชั่นเพียงช่วยในการรักษาขอบเขตรอบ ๆ ซึ่งทำให้เกิดการปิดที่จะสร้าง แต่การบอกว่าการปิดเป็นฟังก์ชั่นที่ไม่ถูกต้องทางเทคนิค ขออภัยที่ nitpick :)
Jason Bunting
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.