你见过的C#或.NET中最奇怪的边角案例是什么?

322

我收集了一些特例和脑筋急转弯,并且总是很愿意听到更多。该页面只涉及C#语言的各种细节,但我也认为核心的.NET知识也很有趣。例如,这里有一个不在页面上的例子,但我觉得它令人难以置信:

string x = new string(new char[0]);
string y = new string(new char[0]);
Console.WriteLine(object.ReferenceEquals(x, y));

我希望输出结果为False——毕竟,“new”(具有引用类型)总是创建一个新对象,不是吗?C#和CLI的规范都表明应该如此。但在这种特殊情况下不是这样。它打印True,并且在我测试过的每个框架版本上都是如此。(我承认我没有在Mono上尝试过...)

仅为明确起见,这只是我正在寻找的示例类型之一——我并不特别寻求对这种奇怪现象的讨论/解释。(它与普通字符串内插不同;��别是,当调用构造函数时,不会通常发生字符串内插。)我真正想问的是是否还有类似奇怪的行为。

是否还有其他隐藏的问题?


64
已测试在Mono 2.0 rc上,返回True。 - Marc Gravell
10
两个字符串最终都变成了 string.Empty,似乎框架只保留了一个对它的引用。 - Adrian Zanescu
34
这是一个内存优化的事情。查找MSDN文档中的静态方法string.Intern。CLR维护了一个字符串池。这就是为什么具有相同内容的字符串显示为引用同一块内存即对象的原因。 - John Leidegren
12
@John说:字符串常量池只会自动对字面量进行池化,但这里不是字面量。@DanielSwe说:让字符串变成不可变的并不一定需要池化,虽然池化也是不可变性的一个好附属效果,但在这里并没有发生普通的池化。 - Jon Skeet
3
导致这种行为的实现细节在这里进行了解释:http://blog.liranchen.com/2010/08/brain-teasing-with-strings.html。 - Liran
显示剩余22条评论
37个回答

394

我想我之前向你展示过这个,但我喜欢这里的趣味 - 这需要一些调试才能追踪到!(原始代码显然更复杂和微妙...)

    static void Foo<T>() where T : new()
    {
        T t = new T();
        Console.WriteLine(t.ToString()); // works fine
        Console.WriteLine(t.GetHashCode()); // works fine
        Console.WriteLine(t.Equals(t)); // works fine

        // so it looks like an object and smells like an object...

        // but this throws a NullReferenceException...
        Console.WriteLine(t.GetType());
    }

所以,什么是Nullable<T> - 比如int?。除了GetType()方法不能被覆盖,其余所有方法都被覆盖;因此它被强制转换(装箱)为对象(因此为null),以调用object.GetType()...这将在空值上调用;-p


更新:情节变得更加扑朔迷离... Ayende Rahien在他的博客上发起了一个相似的挑战, 但是使用了where T : class, new()

private static void Main() {
    CanThisHappen<MyFunnyType>();
}

public static void CanThisHappen<T>() where T : class, new() {
    var instance = new T(); // new() on a ref-type; should be non-null, then
    Debug.Assert(instance != null, "How did we break the CLR?");
}

但是它可以被打败!使用像远程调用等技术所使用的间接方法; 警告 - 以下内容是纯恶意:

class MyFunnyProxyAttribute : ProxyAttribute {
    public override MarshalByRefObject CreateInstance(Type serverType) {
        return null;
    }
}
[MyFunnyProxy]
class MyFunnyType : ContextBoundObject { }

有了这个功能,new()调用将被重定向到代理(MyFunnyProxyAttribute),该代理返回null。现在去洗洗眼睛吧!


9
为什么无法定义 Nullable<T>.GetType()?结果难道不应该是 typeof(Nullable<T>) 吗? - Drew Noakes
69
问题在于GetType()不是虚方法,因此它没有被重写,这意味着该值会被装箱以进行方法调用。装箱后的对象变成了空引用,因此导致了空引用异常。 - Jon Skeet
10
此外,对于 Nullable<T> 类型,还有一些特殊的拳箱规则,这意味着一个空的 Nullable<T> 会被拳箱为 null,而不是包含一个空的 Nullable<T> 的盒子(null 反拳箱为一个空的 Nullable<T>)。 - Marc Gravell
29
非常酷。以一种不酷的方式。;-) - Konrad Rudolph
6
构造函数约束,C# 3.0语言规范中的10.1.5节。 - Marc Gravell
显示剩余10条评论

216

银行家舍入。

这不是编译器错误或故障,但肯定是一个奇怪的角落案例......

.Net框架采用一种称为银行家舍入的舍入方案。

在银行家舍入中,0.5数字会被舍入到最接近的偶数,因此

Math.Round(-0.5) == 0
Math.Round(0.5) == 0
Math.Round(1.5) == 2
Math.Round(2.5) == 2
etc...

这可能会导致基于更为常见的Round-Half-Up舍入法进行的金融计算中出现一些意外的错误。

这也适用于Visual Basic。


22
对我来说,这似乎也很奇怪。也就是说,直到我列了一个大数字列表并计算它们的总和。然后你会意识到,如果你只是简单地四舍五入,你可能会得到与非四舍五入数字之和相差巨大的结果。如果你进行财务计算,这将非常糟糕! - Tsvetomir Tsonev
255
如果有人不知道,你可以使用以下代码:Math.Round(x, MidpointRounding.AwayFromZero);来更改取整方式。 - ICR
26
该方法的行为遵循IEEE标准754第4节。这种舍入方式有时被称为最近舍入或银行家舍入。它最小化了由于在一个方向上一致地舍入中间值而导致的舍入误差。 - ICR
8
我在想这是否是为什么即使在具有内置舍入功能的语言中,我仍经常看到 int(fVal + 0.5) 的原因。 - Ben Blank
32
具有讽刺意味的是,我曾经在一家银行工作过,其他程序员因此开始感到不安,认为舍入在框架中出现了问题。 - dan
显示剩余10条评论

176

如果以 Rec(0) 形式调用此函数(非调试模式),它会执行什么操作?

static void Rec(int i)
{
    Console.WriteLine(i);
    if (i < int.MaxValue)
    {
        Rec(i + 1);
    }
}
  • 在32位JIT上,这应该会导致StackOverflowException
  • 在64位JIT上,它应该打印所有数字直到int.MaxValue

这是因为64位JIT编译器应用了尾调用优化,而32位JIT没有。

不幸的是,我手头没有64位计算机来验证这一点,但该方法符合所有尾调用优化的条件。如果有人有,请告诉我它是否正确。


10
必须在发布模式下编译,但绝对可以在x64上运行 =) - Neil Williams
3
刚在32位WinXP上尝试了VS2010 Beta 1,仍然遇到了StackOverflowException。 - squillman
3
是的,在JIT中支持尾调用只有在编译器生成尾部操作码前缀时才有用,看起来C#编译器仍然没有这样做。 等价的F#代码应该完美地工作。 :) - bcat
130
+1 表示对 StackOverflowException 的赞同。 - calvinlough
7
那个 ++ 彻底把我搞糊涂了。你不能像正常人一样调用 Rec(i + 1) 吗? - configurator
显示剩余11条评论

111

分配!


这是我喜欢在聚会上问的问题(可能这就是为什么我不再被邀请了):

你能让下面这段代码编译通过吗?

    public void Foo()
    {
        this = new Teaser();
    }
一个简单的小技巧可以是:
string cheat = @"
    public void Foo()
    {
        this = new Teaser();
    }
";

但真正的解决方案是这个:

public struct Teaser
{
    public void Foo()
    {
        this = new Teaser();
    }
}

事实上,很少有人知道值类型(结构体)可以重新分配它们的this变量。


3
C++类也可以做到这一点......我最近才发现,但当我试图将其用于优化时被人喊停了 :p - mpen
1
我实际上正在使用就地新建。只是想要一种高效的方法来更新所有字段 :) - mpen
70
这也是一个作弊方法://this = new Teaser(); :-) (注:这行代码在编程中被用作“恶作剧”,它试图将对象的指针 this 修改为一个新的 Teaser 对象的指针,从而影响程序的行为。) - AndrewJacksonZA
17
我宁愿在我的生产代码中使用那些作弊的方法,也不愿意使用这种重新赋值的可怕方法... - Omer Mor
2
从CLR via C#:他们制作这个的原因是因为你可以在另一个构造函数中调用结构体的无参数构造函数。如果您只想初始化结构的一个值,并希望其他值为零/ null(默认值),则可以编写public Foo(int bar){this = new Foo(); specialVar = bar;}。这不是高效的,也不是真正有道理的(specialVar被分配了两次),但只是FYI。(这是书中给出的原因,我不知道为什么我们不应该只做public Foo(int bar) : this() - kizzx2
显示剩余3条评论

100

几年前,在开发忠诚度计划时,我们遇到了一个问题,涉及到将double转换为int的问题。

在下面的代码中:

double d = 13.6;

int i1 = Convert.ToInt32(d);
int i2 = (int)d;

i1 和 i2 是否相等?

结果显示 i1 与 i2 不相等。 由于 Convert 和 cast 操作符中存在不同的舍入策略,实际值如下:

i1 == 14
i2 == 13

调用Math.Ceiling()或Math.Floor()(或使用符合我们要求的MidpointRounding的Math.Round)总是更好的选择。

int i1 = Convert.ToInt32( Math.Ceiling(d) );
int i2 = (int) Math.Ceiling(d);

44
将一个数值转换为整数并不会四舍五入,而是直接截取整数部分(实际上相当于向下取整)。因此这是很合理的。 - Max Schmeling
57
@Max:是的,但为什么Convert要四舍五入? - Stefan Steinegger
18
如果它只是转换,那么一开始就没有必要存在,是吗?另外请注意,该类的名称是Convert而不是Cast。 - bug-a-lot
3
在VB中,CInt()函数会四舍五入,而Fix()函数则是向下取整。这曾经让我吃过亏(http://blog.wassupy.com/2006/01/i-can-believe-it-not-truncating.html)。 - Michael Haren

74
他们本应该使0成为整数,即使存在枚举函数重载。
我知道C#核心团队将0映射到枚举的原因,但它仍然不像应该那样正交。来自Npgsql示例:Npgsql
测试示例:
namespace Craft
{
    enum Symbol { Alpha = 1, Beta = 2, Gamma = 3, Delta = 4 };


   class Mate
    {
        static void Main(string[] args)
        {

            JustTest(Symbol.Alpha); // enum
            JustTest(0); // why enum
            JustTest((int)0); // why still enum

            int i = 0;

            JustTest(Convert.ToInt32(0)); // have to use Convert.ToInt32 to convince the compiler to make the call site use the object version

            JustTest(i); // it's ok from down here and below
            JustTest(1);
            JustTest("string");
            JustTest(Guid.NewGuid());
            JustTest(new DataTable());

            Console.ReadLine();
        }

        static void JustTest(Symbol a)
        {
            Console.WriteLine("Enum");
        }

        static void JustTest(object o)
        {
            Console.WriteLine("Object");
        }
    }
}

18
哇,这对我来说是新鲜事。而且很奇怪的是,ConverTo.ToIn32() 能够运作,但将其强制转换为(int)0就不行了。而任何其他大于0的数字都可以。(所谓“运作”,指的是调用对象重载。) - Lucas
1
@Chris Clark:我尝试在枚举符号上放置None = 0。但编译器仍然选择枚举为0,甚至是(int)0。 - Michael Buen
2
我认为他们应该引入一个关键字“none”,它可以被转换为任何枚举,并使0始终为int,而不是隐式可转换为枚举。 - CodesInChaos
5
ConverTo.ToInt32()之所以有效,是因为其结果不是编译时常量。只有编译时常量0才能转换为枚举类型。在较早版本的.net中,甚至只有字面值0才能转换为枚举类型。请参考Eric Lippert的博客:http://blogs.msdn.com/b/ericlippert/archive/2006/03/28/563282.aspx。 - CodesInChaos
如果你想调用 object 重载,为什么要强制转换成 intJustTest((object)0) 应该会调用正确的方法。 - configurator
显示剩余5条评论

67
这是我目前为止看到的最不寻常的之一(当然,除了这里的其他内容!):
public class Turtle<T> where T : Turtle<T>
{
}

这段代码允许你声明它,但实际上没有什么用处,因为它总是要求你用另一个 Turtle 包装中心的任何类。

[玩笑] 我想这就是所谓的无底龟壳了... [/玩笑]


34
你可以创建实例,例如:class RealTurtle : Turtle<RealTurtle> { } RealTurtle t = new RealTurtle(); - Marc Gravell
24
确实。这是Java枚举类型广泛使用的模式。我也在Protocol Buffers中使用它。 - Jon Skeet
6
RCIX,是的,没错。 - Joshua
8
我在泛型编程中经常使用这个模式。它可以实现正确类型的克隆,或者创建自身实例等操作。 - Lucero
20
这是“奇异递归模板模式(CRTP)”http://en.wikipedia.org/wiki/Curiously_recurring_template_pattern - porges
显示剩余7条评论

65

这是我最近才发现的一个技巧...

interface IFoo
{
   string Message {get;}
}
...
IFoo obj = new IFoo("abc");
Console.WriteLine(obj.Message);

乍一看上面的内容可能有些疯狂,但实际上是合法的。不开玩笑(虽然我错过了一个关键部分,但它不像“添加一个名为IFoo的类”或“添加一个using别名来将IFoo指向一个类”这样的hack)。如果你能弄明白为什么,请看:谁说你不能实例化接口?

1
+1 for "using alias" - 我从来不知道你可以这样做! - David
在编译器中为COM互操作进行黑客攻击 :-) - user90843
你这个混蛋!你至少可以说“在某些情况下”……我的编译器证明了! - M.A. Hanin

56

当一个布尔值既不是True也不是False时,它是什么?

比尔发现你可以“黑掉”一个布尔值,使得如果A为True,B为True,那么(A and B)就变成False。

黑客式布尔值


134
当然是在文件找不到的情况下! - Greg
12
这很有趣,因为这意味着从数学角度来看,C# 中的任何语句都无法被证明。糟糕了。 - Simon Johnson
20
总有一天,我会编写一个依赖于这种行为的程序,那些最黑暗的地狱恶魔们将准备欢迎我的到来。Bwahahahahaha! - Jeffrey L Whitledge
18
这个例子使用的是位运算符,而不是逻辑运算符。这有什么令人惊讶的吗? - Josh Lee
6
他篡改了结构体的布局,当然会得到奇怪的结果,这并不令人惊讶或意外! - user90843
显示剩余2条评论

47

虽然我来晚了一点,但是我有 五个建议:

  1. If you poll InvokeRequired on a control that hasn't been loaded/shown, it will say false - and blow up in your face if you try to change it from another thread (the solution is to reference this.Handle in the creator of the control).

  2. Another one which tripped me up is that given an assembly with:

    enum MyEnum
    {
        Red,
        Blue,
    }
    

    if you calculate MyEnum.Red.ToString() in another assembly, and in between times someone has recompiled your enum to:

    enum MyEnum
    {
        Black,
        Red,
        Blue,
    }
    

    at runtime, you will get "Black".

  3. I had a shared assembly with some handy constants in. My predecessor had left a load of ugly-looking get-only properties, I thought I'd get rid of the clutter and just use public const. I was more than a little surprised when VS compiled them to their values, and not references.

  4. If you implement a new method of an interface from another assembly, but you rebuild referencing the old version of that assembly, you get a TypeLoadException (no implementation of 'NewMethod'), even though you have implemented it (see here).

  5. Dictionary<,>: "The order in which the items are returned is undefined". This is horrible, because it can bite you sometimes, but work others, and if you've just blindly assumed that Dictionary is going to play nice ("why shouldn't it? I thought, List does"), you really have to have your nose in it before you finally start to question your assumption.


6
#2 是一个有趣的例子。枚举类型是编译器对整型数值的映射。因此,即使你没有明确地为它们分配数值,编译器也会为它们分配数值,导致 MyEnum.Red = 0 和 MyEnum.Blue = 1。当你添加了 Black 后,你重新定义了数值 0,将其从 Red 映射到了 Black。我怀疑这个问题在其他用法中也会显现出来,比如序列化。 - LBushkin
3
+1 for Invoke required. 在我们公司,我们更喜欢显式地为枚举分配值,例如 Red=1、Blue=2,这样新的枚举可以在其前面或后面插入,始终会得到相同的值。如果你要保存值到数据库中,这一点尤为重要。 - TheVillageIdiot
53
我不同意将第5种情况视为“边缘情况”。字典应该不基于插入值的时间顺序来定义顺序。如果您想要定义一个顺序,请使用列表,或使用可按有用方式排序的键,或使用完全不同的数据结构。 - Wedge
21
@Wedge,像SortedDictionary一样的数据结构吗? - Allon Guralnek
4
#3发生是因为常量被插入到它们在代码中的每个使用处作为字面值(至少在C#中)。你的前任可能已经注意到这一点,这就是为什么他们使用了只读属性。然而,一个只读变量(与常量相反)同样可以很好地工作。 - Remoun
显示剩余6条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接