C#或.NET中最糟糕的陷阱是什么?

384

我最近在使用一个DateTime对象,并写了类似这样的代码:

DateTime dt = DateTime.Now;
dt.AddDays(1);
return dt; // still today's date! WTF?

AddDays() 的智能感知文档称它会将一天添加到日期上,但实际上它只是返回一个将一天添加后的日期,所以你需要这样写:

DateTime dt = DateTime.Now;
dt = dt.AddDays(1);
return dt; // tomorrow's date

这个问题之前已经让我多次吃过亏,因此我认为记录最严重的C#陷阱会很有用。


158
返回 DateTime.Now 增加一天后的日期。 - crashmstr
24
据我所知,内置值类型都是不可变的,至少在该类型包含的任何方法中,返回的都是一个新项,而不是修改现有项。至少,我想不到有一个不这样做的(方法):所有这些行为都非常一致。 - Joel Coehoorn
6
可变值类型:System.Collections.Generics.List.Enumerator。 (如果你尝试得足够努力,你会看到它表现得很奇怪。) - Jon Skeet
14
智能感知会为你提供所需的所有信息。它表明返回一个DateTime对象。如果它只是改变了你传入的对象,那么它就是一个void方法。 - John Kraft
22
不一定:例如,StringBuilder.Append(...)返回“this”,这在流畅接口中非常常见。 - Jon Skeet
显示剩余10条评论
61个回答

46

DateTime.ToString("dd/MM/yyyy"); 这实际上并不总是给你 dd/MM/yyyy,而是会考虑区域设置并根据您所在的位置替换日期分隔符。因此,您可能会得到类似于 dd-MM-yyyy 的格式。

正确的方法是使用 DateTime.ToString("dd'/'MM'/'yyyy");


DateTime.ToString("r") 应该转换为 RFC1123 格式,使用 GMT。GMT 与 UTC 相差不到一秒钟,然而 "r" 格式说明符 即使 DateTime 是本地时间,也不会转换为 UTC

这会导致以下问题(取决于本地时间与 UTC 相距多远):

DateTime.Parse("Tue, 06 Sep 2011 16:35:12 GMT").ToString("r")
>              "Tue, 06 Sep 2011 17:35:12 GMT"

糟糕!


19
将“mm”更改为“MM” - “mm”表示分钟,“MM”表示月份。我想这是另一个需要注意的问题…… - Kobi
1
我能理解如果你不知道这个问题(我之前也不知道)会让人措手不及,但我在思考什么情况下你需要打印一个与你的区域设置不匹配的日期。 - Beska
6
因为你正在写入文件,所以需要按照特定格式和指定的日期格式来进行。 - GvS
11
我认为将默认设置本地化比反之更糟。如果开发人员完全忽略本地化,代码至少可以在不同语言环境的电脑上正常工作。而现在的做法可能导致代码无法运行。 - Joshua
32
我认为正确的做法是使用DateTime.ToString("dd/MM/yyyy", CultureInfo.InvariantCulture)来进行操作。 - BlueRaja - Danny Pflughoeft

45

我前几天看到这篇文章,感觉内容相当晦涩,对于不了解的人来说可能很痛苦。

int x = 0;
x = x++;
return x;

由于这会返回0而不是大多数人所期望的1


38
我希望它实际上不会咬人,但更希望他们一开始就不要写这个!(当然,这仍然很有趣。) - Jon Skeet
12
我认为这并不是非常晦涩难懂。 - Chris Marasti-Georg
11
在C#中,至少会定义结果,即使结果是出乎意料的。而在C++中,结果可能是0或1,也可能是任何其他结果,包括程序终止! - James Curran
7
这不是个陷阱;x=x++ 的意思是先将 x 赋值给 x,再把 x 值加 1;...而 x=++x 的意思是先将 x 的值加 1,再将新的值赋给 x。请注意不要改变原句的意思,但需要让翻译更通俗易懂。 - Kevin
30
@Kevin: 我认为事情并不是那么简单。如果x=x++等同于x=x后再紧跟着x++,那么结果应该是x=1。相反,我认为首先会计算等号右侧的表达式(得到0),然后对x进行递增(得到x=1),最后执行赋值操作(导致x再次变为0)。 - Tim Goodman
显示剩余16条评论

41

我有点晚来到这个聚会,但最近我遇到了两个问题:

日期时间分辨率

Ticks属性以10百万分之一秒(100纳秒块)为单位测量时间,但分辨率不是100纳秒,而是约15毫秒。

以下是代码:

long now = DateTime.Now.Ticks;
for (int i = 0; i < 10; i++)
{
    System.Threading.Thread.Sleep(1);
    Console.WriteLine(DateTime.Now.Ticks - now);
}

将会给你一个输出(例如):

0
0
0
0
0
0
0
156254
156254
156254

同样地,如果你查看DateTime.Now.Millisecond,你会得到以15.625毫秒为单位的近似值:15、31、46等。

这种特定行为因系统而异,但在这个日期/时间 API 中还有其他分辨率相关的陷阱


Path.Combine

一个很好的组合文件路径的方法,但它并不总是按照你所期望的方式运行。

如果第二个参数以\字符开头,它将不能给你一个完整的路径:

这段代码:

string prefix1 = "C:\\MyFolder\\MySubFolder";
string prefix2 = "C:\\MyFolder\\MySubFolder\\";
string suffix1 = "log\\";
string suffix2 = "\\log\\";

Console.WriteLine(Path.Combine(prefix1, suffix1));
Console.WriteLine(Path.Combine(prefix1, suffix2));
Console.WriteLine(Path.Combine(prefix2, suffix1));
Console.WriteLine(Path.Combine(prefix2, suffix2));

给您这个输出:
C:\MyFolder\MySubFolder\log\
\log\
C:\MyFolder\MySubFolder\log\
\log\

17
每隔约15毫秒量化时间并不是因为基础计时机制存在精度问题(我之前没有详细说明这一点)。而是因为你的应用程序运行在多任务操作系统中。Windows大约每15毫秒检查一次你的应用程序,并在获取到的片段时间内处理自上次片段以来排队的所有消息。由于在同一时间有效地进行了所有调用,因此该片段中的所有调用都返回相同的精确时间。 - MusiGenesis
2
@MusiGenesis:我现在知道它是如何工作的,但是对我来说,拥有一个并不真正精确的那么精确的度量标准似乎是误导性的。这就像是说我知道自己的身高是纳米级别,实际上我只是四舍五入到最近的一千万。 - Damovisa
7
DateTime可以存储到一Tick的精度,问题是DateTime.Now没有使用这种精度。 - Ruben
18
很多Unix/Mac/Linux的使用者都会在这里犯错——当一个反斜杠 \ 多余时。 在Windows中,如果有一个前导的反斜杠 \,它的意思是我们想要去到该驱动器的根目录(例如C:\)。试一下在 CD 命令中输入它会发生什么吧...... 1)进入 C:\Windows\System32 目录 2)键入 CD \Users 3) 哇!现在你来到了 C:\Users ... 明白了吗?... Path.Combine(@"C:\Windows\System32", @"\Users") 应该返回 \Users,这意味着确切地说是 [当前驱动器]:\Users - chakrit
8
即使没有“睡眠”,它的执行方式也是相同的。这与应用程序每15毫秒调度无关。由DateTime.UtcNow调用的本机函数GetSystemTimeAsFileTime似乎具有较低的分辨率。 - Jimbo
2
@Jimbo 说实话,这是一个函数,目的是提供给你当前的时钟时间。如果它只准确到秒,我一点也不会感到意外。 - Luaan

39

使用 System.Diagnostics 启动一个写入控制台的进程时,如果您从未读取 Console.Out 流,在输出一定量的内容后,您的应用程序将似乎停止响应。


3
如果你重定向了标准输出和标准错误,并且使用了两个连续的ReadToEnd调用函数,同样可能会出现问题。为了安全地处理标准输出和标准错误,你需要为它们各自创建一个读取线程。 - Sebastiaan M

36

Linq-To-Sql中没有运算符快捷方式

请参见这里

简而言之,在Linq-To-Sql查询的条件子句中,您不能使用条件快捷方式,如||&&来避免空引用异常。即使第一个条件导致无需评估第二个条件,Linq-To-Sql也会评估OR或AND运算符的两侧!


9
今天我学到了一件事(TIL),我需要重新优化几百个 LINQ 查询,稍后回来(BRB)。 - tsilb

31

使用虚方法的默认参数

abstract class Base
{
    public virtual void foo(string s = "base") { Console.WriteLine("base " + s); }
}

class Derived : Base
{
    public override void foo(string s = "derived") { Console.WriteLine("derived " + s); }
}

...

Base b = new Derived();
b.foo();

输出:
派生基类


10
奇怪,我认为这是完全明显的。如果声明类型为“Base”,那么编译器除了从“Base”中获取默认值之外,还应该从哪里获取呢?我认为更令人困惑的是,即使调用的方法(静态地)是基类方法,但如果声明类型是派生类型,则默认值可以不同。 - Timwi
1
为什么一个方法的实现会得到另一个实现的默认值? - staafl
1
默认参数在编译时而非运行时解析。 - fredoverflow
2
我认为这个 gotcha 通常出现在默认参数中 - 人们经常没有意识到它们是在编译时解析的,而不是运行时。 - Luaan
4
@FredOverflow,我的问题是概念性的。尽管行为与实现相关,但它不直观且很可能会导致错误。在我看来,C#编译器不应该允许在覆盖方法时更改默认参数值。 - staafl
显示剩余6条评论

28

可变集合中的值对象

struct Point { ... }
List<Point> mypoints = ...;

mypoints[i].x = 10;

没有影响。

mypoints[i] 返回 Point 值对象的一个副本。C# 乐意让你修改这个副本的字段,而不会有任何提示。


更新: 这个问题在 C# 3.0 中已经得到修复:

Cannot modify the return value of 'System.Collections.Generic.List<Foo>.this[int]' because it is not a variable

6
我可以理解这种困惑,因为它确实适用于数组(与您的答案相反),但不适用于其他动态集合,例如List<Point>。 - Lasse V. Karlsen
2
你说得对。谢谢。我已经修正了我的回答:)。arr[i].attr = 是数组的特殊语法,你无法在库容器中编写它 ;(。为什么允许使用 (<value expression>).attr = <expr>?它有意义吗? - Bjarke Ebert
1
@Bjarke Ebert:有些情况下这样做是有意义的,但不幸的是编译器无法识别和允许这些情况。使用场景示例:一个不可变的结构体,它持有对一个正方形二维数组的引用以及一个“旋转/翻转”指示器。结构体本身将是不可变的,因此写入只读实例的元素应该是可以的,但编译器不知道属性设置器实际上并不会写入结构体,因此不会允许它。 - supercat

26

也许不是最糟糕的,但 .net框架的某些部分使用度数,而其他部分则使用弧度(并且与Intellisense一起显示的文档从不告诉您哪种方式,您必须访问MSDN才能找到)

通过拥有一个Angle类,所有这些都可以避免...


我很惊讶这篇文章获得了这么多的赞,因为我的其他陷阱比这个更糟糕。 - BlueRaja - Danny Pflughoeft

22

对于C/C++程序员来说,转向C#是很自然的。然而,在个人经验以及看到其他人做同样转换的过程中,我遇到的最大问题(也见过其他人遇到同样的问题)是不完全理解在C#中类和结构体之间的区别。

在C++中,类和结构体是相同的;它们只在默认可见性方面有所不同,其中类的默认可见性为private,而结构体的默认可见性为public。在C++中,以下类定义

    class A
    {
    public:
        int i;
    };

这与该结构定义在功能上是等效的。

    struct A
    {
        int i;
    };

在C#中,类是引用类型,而结构体是值类型。这使得在(1)决定何时使用其中之一、(2)测试对象相等性、(3)性能方面(例如,装箱/拆箱)等方面存在重大差异。

网络上有各种与两者之间差异相关的信息(例如,此处)。我强烈鼓励任何转向C#的人至少了解它们之间的差异及其影响。


13
那么,最容易被陷入的陷阱就是人们在使用语言之前不花时间学习它吗? - BlueRaja - Danny Pflughoeft
4
更像是一个经典陷阱,看似相似的语言使用非常相似的关键词和语法,但实际上工作方式却有很大不同。 - Luaan

19

数组实现了 IList

但是它没有真正实现它。当你调用 Add 方法时,它会告诉你无法工作。那么为什么一个类会实现一个它无法支持的接口呢?

编译通过,但不可用:

IList<int> myList = new int[] { 1, 2, 4 };
myList.Add(5);

我们经常遇到这个问题,因为序列化程序(WCF)将所有的ILists转换成数组,然后我们就会遇到运行时错误。


10
在我看来,问题在于Microsoft没有为集合定义足够的接口。我认为它应该有iEnumerable、iMultipassEnumerable(支持Reset,并保证多次通过匹配)、iLiveEnumerable(如果集合在枚举期间更改,则将具有部分定义的语义——更改可能会出现在枚举中,但不应导致虚假结果或异常)、iReadIndexable、iReadWriteIndexable等接口。因为接口可以“继承”其他接口,所以这并不需要太多额外的工作,甚至可能会节省NotImplemented存根。 - supercat
@supercat,这对于初学者和某些长期编码人员来说会非常困惑。我认为.NET集合及其接口非常优雅。但我很欣赏你的谦虚。 ;) - Jordan
@Jordan:自我上述内容以来,我已经决定采用更好的方法,即让IEnumerable<T>IEnumerator<T>都支持一个“特性”属性,以及一些“可选”的方法,其有用性将由“特性”报告确定。尽管如此,我仍然坚持我的主要观点,即在某些情况下,接收IEnumerable<T>的代码需要比IEnumerable<T>提供的更强的承诺。调用ToList将产生一个遵守这些承诺的IEnumerable<T>,但在许多情况下会是不必要的昂贵。我认为应该... - supercat
这是一种方法,使得接收 IEnumerable<T> 的代码可以在需要时复制其内容,但不必要时可以避免这样做。 - supercat
你还假设他们可以放弃一直延续到.NET创建的命名空间并重新开始。这是不可能发生的。我相信这种奇怪的用法是某些要求的产物,否则就需要重写。 - Jordan
显示剩余6条评论

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