提问人:Developer 提问时间:1/10/2009 最后编辑:Drag and DropDeveloper 更新时间:1/10/2023 访问量:49163
什么是 .NET 中的“闭包”?
What are 'closures' in .NET?
答:
闭包是保留其原始作用域中的变量值的功能值。C# 可以以匿名委托的形式使用它们。
举一个非常简单的例子,以这个 C# 代码为例:
delegate int testDel();
static void Main(string[] args)
{
int foo = 4;
testDel myClosure = delegate()
{
return foo;
};
int bar = myClosure();
}
最后,bar 将设置为 4,并且可以传递 myClosure 委托以在程序中的其他位置使用。
闭包可以用于许多有用的事情,例如延迟执行或简化接口 - LINQ 主要使用闭包构建。对于大多数开发人员来说,最直接的方法是将事件处理程序添加到动态创建的控件 - 您可以使用闭包在实例化控件时添加行为,而不是将数据存储在其他地方。
我有一篇关于这个主题的文章。(它有很多例子。
从本质上讲,闭包是一个代码块,可以在以后执行,但它维护了它最初创建它的环境 - 即它仍然可以使用创建它的方法的局部变量等,即使在该方法完成执行之后。
闭包的一般功能在 C# 中通过匿名方法和 lambda 表达式实现。
下面是使用匿名方法的示例:
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
闭包是在函数中定义的函数,可以访问函数的局部变量及其父变量。
public string GetByName(string name)
{
List<things> theThings = new List<things>();
return theThings.Find<things>(t => t.Name == name)[0];
}
所以 find 方法里面的函数。
t => t.Name == name
可以访问其作用域 T 中的变量,以及其父作用域中的变量名称。即使它由 find 方法作为委托执行,也要从另一个作用域一起执行。
评论
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
闭包是在创建它的函数之外传递的匿名函数。 它维护它使用的创建它的函数中的任何变量。
闭包是引用自身外部变量的代码块(从堆栈上的变量下方),以后可能会调用或执行(例如,当定义事件或委托时,可能会在某个不确定的未来时间点被调用)......因为代码块引用的外部变量可能超出了范围(否则会丢失),所以它被代码块引用的事实(称为闭包)告诉运行时在作用域中“保留”该变量,直到闭包代码块不再需要它......
评论
下面是我根据 JavaScript 中的类似代码创建的 C# 示例:
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
希望这有点帮助。
评论
基本上,闭包是一个代码块,您可以将其作为参数传递给函数。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);
评论
如果你有兴趣了解 C# 如何实现 Closure,请阅读“我知道答案(它的 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);
}
}
评论
闭包是指在另一个函数(或方法)中定义一个函数,并且它使用父方法中的变量。这种使用位于方法中并包装在其中定义的函数中的变量称为闭包。
Mark Seemann 在他的博客文章中提供了一些有趣的闭包示例,他在其中将 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.
出乎意料的是,C# 7.0 简而言之,这是一本简单易懂的答案。
你应该知道的先决条件:lambda 表达式可以引用方法的局部变量和参数 在其中定义它(外部变量)。
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
}
实部:lambda 表达式引用的外部变量称为捕获变量。捕获变量的 lambda 表达式称为闭包。
需要注意的最后一点:捕获的变量是在实际调用委托时计算的,而不是在捕获变量时计算的:
int factor = 2;
Func<int, int> multiplier = n => n * factor;
factor = 10;
Console.WriteLine (multiplier (3)); // 30
如果您编写内联匿名方法 (C#2) 或(最好)Lambda 表达式 (C#3+),则仍在创建实际方法。如果该代码使用的是外部范围的局部变量 - 您仍然需要以某种方式将该变量传递给该方法。
例如,采用以下 Linq Where 子句(这是一个传递 lambda 表达式的简单扩展方法):
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;
});
如果要在该 lambda 表达式中使用 i,则必须将其传递给该 created 方法。
因此,出现的第一个问题是:它应该通过值还是引用来传递?
通过引用传递(我猜)更可取,因为您可以获得对该变量的读/写访问权限(这就是 C# 所做的;我猜Microsoft的团队权衡了利弊,并参考了;根据 Jon Skeet 的文章,Java 采用了 by-value)。
但随之而来的是另一个问题:在哪里分配那个 i?
它是否应该实际/自然地分配在堆栈上? 好吧,如果你在堆栈上分配它并通过引用传递它,在某些情况下,它可能会超过它自己的堆栈帧。举个例子:
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;
});
}
lambda 表达式(在 Where 子句中)再次创建一个引用 i 的方法。如果 i 被分配在 Outlive 的堆栈上,那么当你枚举 whereItems 时,生成的方法中使用的 i 将指向 Outlive 的 i,即堆栈中无法再访问的位置。
好的,那么我们需要它在堆上。
因此,C# 编译器支持这种内联匿名/lambda 所做的是使用所谓的“闭包”:它在堆上创建一个名为(相当糟糕的)DisplayClass 的类,该类有一个包含 i 的字段,以及实际使用它的函数。
与此等效的东西(您可以看到使用 ILSpy 或 ILDASM 生成的 IL):
class <>c_DisplayClass1
{
public int i;
public bool <GetFunc>b__0()
{
this.i++;
Console.WriteLine(i);
return true;
}
}
它在本地作用域中实例化该类,并将与 i 或 lambda 表达式相关的任何代码替换为该闭包实例。因此,每当您在定义 i 的“本地范围”代码中使用 i 时,您实际上都在使用该 DisplayClass 实例字段。
因此,如果我在 main 方法中更改“本地”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”转到该 dispalyclass 字段并在第二个枚举之前对其进行更改。
关于该主题的一个很好的来源是这个 Bart De Smet Pluralsight 模块(需要注册)(也忽略他对术语“提升”的错误使用——(我认为)他的意思是局部变量(即 i)被更改为引用新的 DisplayClass 字段)。
在其他新闻中,似乎有一些误解,认为“闭包”与循环有关——据我所知,“闭包”不是一个与循环相关的概念,而是与匿名方法/lambda 表达式使用局部作用域变量有关——尽管一些技巧问题使用循环来演示它。
闭包旨在简化函数式思维,并允许运行时管理 状态,为开发人员释放额外的复杂性。闭包是一流的函数 使用在词法环境中绑定的自由变量。这些流行语的背后 隐藏了一个简单的概念:闭包是授予函数访问权限的更方便的方式 到本地状态,并将数据传递到后台操作。它们是特殊功能 对所有非局部变量(也称为自由变量或 up-values)。此外,闭包允许函数访问一个或多个非局部变量,即使在其直接词法范围之外调用时,也允许函数访问正文 这个特殊函数可以将这些自由变量作为单个实体进行传输,定义在 它的封闭范围。更重要的是,闭包封装了行为并传递了它 像任何其他对象一样,授予对闭包所在的上下文的访问权限 创建、读取和更新这些值。
评论