我收集了一些极端案例和脑筋急转弯,总是想听到更多。这个页面只涵盖了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上尝试过……)
只是为了澄清,这只是我正在寻找的事情的一个例子-我并不是特别寻找对这个奇怪现象的讨论/解释。(这和普通的弦乐实习不一样;特别地,当调用构造函数时,字符串实习通常不会发生。)我真的是在要求类似的奇怪行为。
还有其他的宝藏吗?
银行家的舍入。
这不是一个编译器错误或故障,但肯定是一个奇怪的极端情况…
. net框架采用了一种被称为银行家舍入的方案或舍入。
在银行家的四舍五入中,0.5的数字四舍五入到最接近的偶数,所以
Math.Round(-0.5) == 0
Math.Round(0.5) == 0
Math.Round(1.5) == 2
Math.Round(2.5) == 2
etc...
这可能会导致基于更广为人知的四舍五入的财务计算中出现一些意想不到的错误。
Visual Basic也是如此。
有趣的是,当我第一次看到它时,我认为这是c#编译器正在检查的东西,但即使你直接发出IL来消除任何干扰的机会,它仍然会发生,这意味着它真的是newobj操作代码正在进行检查。
var method = new DynamicMethod("Test", null, null);
var il = method.GetILGenerator();
il.Emit(OpCodes.Ldc_I4_0);
il.Emit(OpCodes.Newarr, typeof(char));
il.Emit(OpCodes.Newobj, typeof(string).GetConstructor(new[] { typeof(char[]) }));
il.Emit(OpCodes.Ldc_I4_0);
il.Emit(OpCodes.Newarr, typeof(char));
il.Emit(OpCodes.Newobj, typeof(string).GetConstructor(new[] { typeof(char[]) }));
il.Emit(OpCodes.Call, typeof(object).GetMethod("ReferenceEquals"));
il.Emit(OpCodes.Box, typeof(bool));
il.Emit(OpCodes.Call, typeof(Console).GetMethod("WriteLine", new[] { typeof(object) }));
il.Emit(OpCodes.Ret);
method.Invoke(null, null);
如果检查string,它也等于true。这意味着这个操作代码必须有特殊的行为来实习空字符串。
在我们使用的API中,返回域对象的方法可能返回一个特殊的“空对象”。在此实现中,比较运算符和Equals()方法将被重写,如果与null进行比较则返回true。
所以这个API的用户可能会有这样的代码:
return test != null ? test : GetDefault();
或者更啰嗦一点,像这样:
if (test == null)
return GetDefault();
return test;
其中GetDefault()是一个方法,返回一些我们想要使用的默认值,而不是null。当我使用ReSharper并按照它的建议重写这其中的任何一个时,我感到惊讶:
return test ?? GetDefault();
如果测试对象是从API返回的空对象,而不是一个正确的空对象,那么代码的行为现在已经改变,因为空合并操作符实际上检查null,而不是运行operator=或Equals()。
以下可能是我缺乏的常识,但是,嗯。前段时间,我们遇到了一个包含虚拟财产的bug案例。将上下文抽象一点,考虑以下代码,并将断点应用到指定区域:
class Program
{
static void Main(string[] args)
{
Derived d = new Derived();
d.Property = "AWESOME";
}
}
class Base
{
string _baseProp;
public virtual string Property
{
get
{
return "BASE_" + _baseProp;
}
set
{
_baseProp = value;
//do work with the base property which might
//not be exposed to derived types
//here
Console.Out.WriteLine("_baseProp is BASE_" + value.ToString());
}
}
}
class Derived : Base
{
string _prop;
public override string Property
{
get { return _prop; }
set
{
_prop = value;
base.Property = value;
} //<- put a breakpoint here then mouse over BaseProperty,
// and then mouse over the base.Property call inside it.
}
public string BaseProperty { get { return base.Property; } private set { } }
}
而在Derived对象上下文中,您可以在添加基类时获得相同的行为。属性作为手表,或打字基地。财产成快表。
我花了些时间才意识到发生了什么。最后,我受到了Quickwatch的启发。当进入Quickwatch并探索派生对象d(或从对象的上下文,this)并选择字段基时,Quickwatch顶部的edit字段显示以下类型转换:
((TestProject1.Base)(d))
这意味着如果base被替换,调用会是
public string BaseProperty { get { return ((TestProject1.Base)(d)).Property; } private set { } }
对于watch、Quickwatch和调试鼠标移到工具提示,在考虑多态性时显示“AWESOME”而不是“BASE_AWESOME”是有意义的。我仍然不确定为什么它会转换成一个类型转换,一个假设是,调用可能无法从这些模块的上下文,只有callvirt。
无论如何,这显然不会改变派生功能方面的任何东西。BaseProperty仍然会返回“BASE_AWESOME”,因此这不是我们工作中的bug的根源,只是一个令人困惑的组件。然而,我发现有趣的是,它会误导开发人员,他们在调试过程中不会意识到这一事实,特别是如果Base没有在你的项目中公开,而是作为第三方DLL引用,导致开发人员只是说:
“喂,等等……什么?”omg那个DLL是
, . .做一些有趣的事情”
你有没有想过c#编译器会生成无效的CIL?运行这个,你会得到一个typeloadeexception:
interface I<T> {
T M(T p);
}
abstract class A<T> : I<T> {
public abstract T M(T p);
}
abstract class B<T> : A<T>, I<int> {
public override T M(T p) { return p; }
public int M(int p) { return p * 2; }
}
class C : B<int> { }
class Program {
static void Main(string[] args) {
Console.WriteLine(new C().M(42));
}
}
但我不知道它在c# 4.0编译器中表现如何。
编辑:这是从我的系统输出:
C:\Temp>type Program.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ConsoleApplication1 {
interface I<T> {
T M(T p);
}
abstract class A<T> : I<T> {
public abstract T M(T p);
}
abstract class B<T> : A<T>, I<int> {
public override T M(T p) { return p; }
public int M(int p) { return p * 2; }
}
class C : B<int> { }
class Program {
static void Main(string[] args) {
Console.WriteLine(new C().M(11));
}
}
}
C:\Temp>csc Program.cs
Microsoft (R) Visual C# 2008 Compiler version 3.5.30729.1
for Microsoft (R) .NET Framework version 3.5
Copyright (C) Microsoft Corporation. All rights reserved.
C:\Temp>Program
Unhandled Exception: System.TypeLoadException: Could not load type 'ConsoleAppli
cation1.C' from assembly 'Program, Version=0.0.0.0, Culture=neutral, PublicKeyTo
ken=null'.
at ConsoleApplication1.Program.Main(String[] args)
C:\Temp>peverify Program.exe
Microsoft (R) .NET Framework PE Verifier. Version 3.5.30729.1
Copyright (c) Microsoft Corporation. All rights reserved.
[token 0x02000005] Type load failed.
[IL]: Error: [C:\Temp\Program.exe : ConsoleApplication1.Program::Main][offset 0x
00000001] Unable to resolve token.
2 Error(s) Verifying Program.exe
C:\Temp>ver
Microsoft Windows XP [Version 5.1.2600]