译自Eric Lippert's Blog, 原文: http://blogs.msdn.com/ericlippert/archive/2009/08/27/what-s-the-difference-between-fixed-and-fixed.aspx
某天我收到一封这样开头的邮件:
我有一个关于C# 固定大小的缓冲区的问题:
unsafe struct FixedBuffer { public fixed int buffer[100]; }
现在把缓冲区声明为固定的,它就是不可移动的...
看到这个问题我的心都碎了。如果语言细节设计容易造成误解,那么上面所遇到的情况只是极为不幸的时刻之一。
当在非安全的代码中对托管对象使用指针算法时,你要确保垃圾回收器不会把你正需要的内存移掉。当你正在用指针操作某个对象的时候,如果另外一个线程进行了资源回收,那么这个指针将完全混乱。因此,C#把变量分为“固定的”和“可变的”。如果你想要将指针应用到一个可移动的对象上,你可以使用“fixed”关键字来声明“这个局部变量是不能被垃圾回收器移动的”。当回收行为发生时,垃圾回收器要为那些正在进行的调用查看所有局部变量(那些被用到的变量需要保留);如果回收器看到某变量被标为“fixed”,它不会移动这样的变量,即使这会造成托管堆碎片(由此也看出让这样的变量固定的时间尽可能少是很重要的)。所以典型地,我们用“fixed”代表固定在某个地方。
但是在这封邮件中的“fixed”并不是这个意思,这里的意思是将此问题中的buffer的大小固定为容纳100个int变量,本质上讲,这和在结构中创建100个int类型的成员是一样的。
明显地,我们经常使用相同的关键字来表示概念上相同的东西。比如,在C#中我们经常用不同的方式使用关键字“internal”,但是所有的“internal”表示的意义都是一样的。它只是用来表示“一些实体的访问权限在同一程序集的代码中是不受限制的”。
某些时候我们也用相同的关键字来表示概念上完全不同的东西,这要依赖于用户使用的上下文环境来确定它代表的意思。比如:
var results = from c in customers where c.City == "London" select c;
和
class C<T> where T : IComparable<T>
很明显“where”使用于两种完全不同的方式:建立查询语句中的筛选从句,和声明一个泛型参数的约束类型。
如果一个关键字用于两种不同的意义但是这个区别非常细微,就像我们上面举到的例子,这会使人们碰到困难。那个用户的邮件将会继续问上一大堆的问题,但是这些问题都是基于一个不正确的假设——一个固定大小的缓冲区会自动地固定在内存中的某一位置。
这只能说这是一个不好的术语的混用:“固定大小”和“固定位置”都是使用“fixed”,但是使用的方式不同,这是很让人头疼的事情。然而它们之间的关联比这还要复杂:只有当某固定大小的缓冲区的容器被固定在某块内存区,你才能安全地访问存储在该缓冲区中的数据。在这个问题中,这两个概念具有很强的相关性,但是并不完全相同。
一方面,为了减少混乱,我们可以使用两个不同的关键字,比如说“pinned”和“fixed”。但是另一方面,“fixed”的所有用法只在非安全代码中才可用。对于非安全的码的所有特性,一个关键的前提假设就是:如果你想在C#中使用非安全代码,那么你肯定已经是一个完全理解CLR中内存管理的编程专家。这也是为什么我们让你在代码中标明“unsafe”的原因;它表明你关闭了安全系统并且知道自己在做什么。
在C#中可以多用的一些重要的关键字:fixed, into, partial, out, in, new, delegate, where, using, class, struct, true, false, base, this, event, return和void都至少包含两种以上不同的意思。大部分的在位于特定上下文中的时候都很清楚,但是至少前三个——fixed, into和partial——已经造成了很多的困扰,我已经收到了很多困惑的用户关于它们使用区别的问题。我接下来会看一下“into”和“partial”。
译自Eric Lippert's Blog, 原文: http://blogs.msdn.com/ericlippert/archive/2009/08/03/what-s-the-difference-part-two-scope-vs-declaration-space-vs-lifetime.aspx
在编程语言设计中,作用域(scope)通常是最容易混淆的概念。
人们似乎不经意间就使用这个概念,我经常看到它被当做生存期(lifetime)或定义空间(declaration space)使用,例如"当这个变量超出作用域时,对应的内存空间将被释放"。当然在非正式场合,只要听众能清楚地理解你所指的意思,使用"作用域"来表示是完全可以接受的。但是在更正式的场合,比如书中或语言标准中,就需要更精确地使用这些概念。
C#中作用域和定义空间的区别是微妙的。
一个命名实体(entity)的作用域是源代码中可以合法使用它的非限定名(unqualified name)来引用它的范围。
这里有些微妙的东西, 这个定义并没有别的隐含的意思 -- 它并非表示如果你可以合法使用一个实体的非限定名,则通过这个非限定名就可以引用这个实体。因为作用域是允许重叠(overlap)的。例如,如果有以下代码:
class C
{
int x;
void M()
{
int x;
}
}
字段x(field)的作用域是源代码中类C(class)的整个定义部分,包括方法M(method)的整个定义;局部变量x(local variable)的作用域是方法M的主体部分。所以这两个实体的作用域有重叠的部分。当你在不同的位置使用非限定名"x",将得到不同的实体(字段x或者局部变量x)。
相反, 定义空间是一段其中不允许存在具有相同名称实体的源代码范围。例如,在类C的定义部分,除了方法M的主体部分,不允许别的实体也命名为x。一旦定义了个字段x, 就不能再定义别的称为x的字段,属性(property), 内嵌类型(nested type)或事件(event)。
正因为有重载机制(overload), 使得方法有一点特殊。可以特别定义有方法的定义空间为"一个类中所有具有相同名称的重载方法构成一个实体", 也可以重新定义定义空间为"定义空间中不允许存在具有相同名称的实体, 除了具有不同签名(signature)的方法"。
简短地说,作用域解决了“在哪里可以使用这个名称”的问题;命名空间解决了“这个名称在哪里是唯一的”的问题。
生存期和作用域经常在局部变量上容易混淆,因为在局部变量上这两者关系太复杂了。扼要地讲,就是只要当前的执行点(point of execution)在局部变量的作用域内,至少能保证它的内容可用。但也可能局部变量的内容在超出它的作用域外也是可用的,比如捕获变量(capture variable, variable capturing , 在匿名函数中使用外部变量 )将扩展这个变量的生存期。
译自Eric Lippert's Blog, 原文: http://blogs.msdn.com/ericlippert/archive/2009/07/30/generics-are-not-templates.aspx
因为我不是常人, 所以我喜欢去了解容易混淆的东西间的微妙差别:
- 我的脑袋里还是非常地不明白集线器,路由器和交换机之间的区别,并且也不明白他们怎么在里面联系起来的。
- 找到的大块的矿石事实上是岩石;只要你把它们用于花园或建造一座桥梁, 它们就突然变成了石料。
- 当一只猪达到了120磅,它就是一只肥猪。
我想我可以为编程语言设计中一些容易困惑的概念做一个小系列.
这里是一个我经常遇到的问题:
public class C
{
public static void DoIt<T>(T t)
{
ReallyDoIt(t);
}
private static void ReallyDoIt(string s)
{
System.Console.WriteLine("string");
}
private static void ReallyDoIt<T>(T t)
{
System.Console.WriteLine("everything else");
}
}
当你调用C.DoIt<string>会发生什么呢?很多人希望打印出“string”, 然而不管T是什么,事实上总是打印出“everything else”。
C#语言标准说当你需要面临选择调用ReallyDoIt<string>(string)或是ReallyDoIt(string)的时候,也就是说在需要从两个相同签名方法选择, 其中包含一个是泛型方法的时候,我们会选择非泛型的方法而不是泛型的方法。但在这里我们为什么不这么做呢?
因为那不是标准所说的选择。如果你说要调用ReallyDoIt("hello world");
那么我们会选择非泛型的版本。但是你并不是把编译器所知道的string类型进行参数传递。你只是传递了类型T,一个没有约束的类型参数,所以它可以是任何类型。所以由于重载决议的算法,有没有一个方法能接收任何类型作为参数呢?回答是有。
这个C#里泛型的例子并不像C++里面的模板。你可以把模板想象成一个奢华的搜索替代的机制。当你说你要在模板中调用DoIt<string>, 编译器会概念性地搜索出所有用到的“T”,再把它们替换成“string” ,接着再编译源代码。重载决议用替代的已知类型参数来执行,生成的代码也反映了重载决的结果。
泛型可不是这么工作的。泛型是这样的,好吧,一般的。我们只做一次重载决议然后固定了结果。当任何程序从一个完全不同的程序集用string作为类型参数调用到这个方法的时候,我们不会在运行时改变它。我们为泛型生成的IL代码已经定好了将要去选出调用的方法。那个JIT并不是说“好吧,如果我们让C#编译器用这个额外信息去马上执行我碰巧就知道了,那么它就会挑一个不同的重载. 让我重写一下生成的(IL)代码吧,让编译器忽略以前生成的”。 JIT是不知道任何C#的条条框框的。
本质上来说,上面的例子和这个没有区别:
public class C
{
public static void DoIt(object t)
{
ReallyDoIt(t);
}
private static void ReallyDoIt(string s)
{
System.Console.WriteLine("string");
}
private static void ReallyDoIt(object t)
{
System.Console.WriteLine("everything else");
}
}
当编译器为调用ReallyDoIt生成(IL)的时候, 它会挑选object作为参数的版本,因为那是它最好的选择。如果有人用string来调用方法,那么它还是会选择object作为参数的版本。
现在,如果你确实想根据参数的类型在运行时做重载决议,我们能为你做到。那就是C# 4.0的新特性dynamic能做的。只要把object替换成dynamic, 那么当你调用那个object的方法时, 我们会在运行时根据它所知道的所有的运行时类型,执行重载决议并且动态地生成编译器选择的调用代码。