การปิดคืออะไร เรามีไว้ใน. NET หรือไม่
หากมีอยู่ใน. NET คุณช่วยอธิบายข้อมูลโค้ด (โดยเฉพาะอย่างยิ่งใน C #) ได้ไหม?
การปิดคืออะไร เรามีไว้ใน. NET หรือไม่
หากมีอยู่ใน. NET คุณช่วยอธิบายข้อมูลโค้ด (โดยเฉพาะอย่างยิ่งใน C #) ได้ไหม?
คำตอบ:
ผมมีบทความในหัวข้อนี้มาก (มีตัวอย่างมากมาย)
ในสาระสำคัญการปิดเป็นบล็อกของรหัสที่สามารถดำเนินการได้ในภายหลัง แต่ที่รักษาสภาพแวดล้อมที่มันถูกสร้างขึ้นครั้งแรก - มันยังคงสามารถใช้ตัวแปรท้องถิ่น ฯลฯ ของวิธีการที่สร้างมันแม้หลังจากนั้น วิธีการดำเนินการเสร็จสิ้น
คุณลักษณะทั่วไปของการปิดถูกนำมาใช้ใน 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 นั้นจะเสร็จสิ้น
counter
สามารถเพิ่มค่าได้ - คอมไพเลอร์สร้างคลาสที่มีcounter
ฟิลด์และโค้ดใด ๆ ที่อ้างอิงถึงการcounter
เข้าสู่อินสแตนซ์ของคลาสนั้น
หากคุณสนใจที่จะดูว่า 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);
}
}
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 นั้นส่วนใหญ่สร้างขึ้นโดยใช้การปิด วิธีที่สะดวกที่สุดสำหรับนักพัฒนาส่วนใหญ่คือการเพิ่มตัวจัดการเหตุการณ์ไปยังตัวควบคุมที่สร้างขึ้นแบบไดนามิกคุณสามารถใช้การปิดเพื่อเพิ่มพฤติกรรมเมื่อมีการสร้างอินสแตนซ์แทนที่จะเก็บข้อมูลไว้ที่อื่น
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
การปิดเป็นฟังก์ชันที่ไม่ระบุตัวตนซึ่งถูกส่งไปภายนอกฟังก์ชั่นที่สร้างขึ้น มันเก็บรักษาตัวแปรใด ๆ จากฟังก์ชั่นที่มันถูกสร้างขึ้นที่จะใช้
นี่คือตัวอย่างที่วางแผนไว้สำหรับ 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
หวังว่าจะเป็นประโยชน์บ้าง
Closures คือโค้ดที่อ้างอิงตัวแปรภายนอกตัวเอง (จากด้านล่างพวกมันบนสแต็ก) ซึ่งอาจถูกเรียกใช้หรือเรียกใช้ในภายหลัง (เช่นเมื่อมีการกำหนดเหตุการณ์หรือผู้ได้รับมอบหมายและสามารถเรียกได้ที่จุดอนาคตไม่แน่นอน ) ... เนื่องจากตัวแปรภายนอกที่กลุ่มของการอ้างอิงรหัสอาจหายไปจากขอบเขต (และอาจสูญหายไป) ความจริงที่ว่ามันถูกอ้างอิงโดยกลุ่มของรหัส (เรียกว่าการปิด) บอกรันไทม์เพื่อ "ถือ "ตัวแปรนั้นอยู่ในขอบเขตจนกว่าจะไม่จำเป็นต้องใช้โดยการปิดโค้ดอีกต่อไป ...
การปิดโดยทั่วไปคือบล็อกของรหัสที่คุณสามารถส่งเป็นอาร์กิวเมนต์ให้กับฟังก์ชันได้ 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);
ปิดคือเมื่อฟังก์ชั่นที่มีการกำหนดภายในฟังก์ชั่นอื่น (หรือวิธีการ) และจะใช้ตัวแปรจากวิธีการปกครอง การใช้ตัวแปรซึ่งตั้งอยู่ในวิธีการและห่อหุ้มในฟังก์ชั่นที่กำหนดไว้ภายในนั้นเรียกว่าการปิด
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.
ฉันได้พยายามที่จะเข้าใจเช่นกันด้านล่างนี้เป็นข้อมูลโค้ดสำหรับรหัสเดียวกันใน Javascript และ C # ที่แสดงการปิด
จาวาสคริปต์:
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);
}
}
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);
}
}
เพิ่งออกมาจากสีน้ำเงินคำตอบที่เข้าใจง่ายและเข้าใจง่ายกว่าจากหนังสือ 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
หากคุณเขียนวิธีการแบบไม่ระบุชื่อแบบอินไลน์ (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 ใหม่)
ในข่าวอื่น ๆ ดูเหมือนว่ามีความเข้าใจผิดว่า "การปิด" เกี่ยวข้องกับลูป - เนื่องจากฉันเข้าใจว่า "การปิด" ไม่ใช่แนวคิดที่เกี่ยวข้องกับลูปแต่เป็นวิธีการที่ไม่ระบุชื่อ / การแสดงออกของแลมบ์ดาใช้ตัวแปรท้องถิ่น คำถามใช้ลูปเพื่อแสดงให้เห็น
การปิดเป็นฟังก์ชั่นที่กำหนดไว้ในฟังก์ชั่นที่สามารถเข้าถึงตัวแปรท้องถิ่นของมันเช่นเดียวกับผู้ปกครองของมัน
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 และชื่อตัวแปรที่อยู่ในขอบเขตพาเรนต์ แม้ว่าจะถูกดำเนินการโดยวิธีการค้นหาในฐานะผู้รับมอบสิทธิ์จากขอบเขตอื่นทั้งหมดเข้าด้วยกัน