在C#或.NET中最糟糕的陷阱是什么?[关闭]


377

我最近正在处理一个DateTime对象,并编写了如下内容:

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

的intellisense文档AddDays()说,它在日期上添加了一天,但没有添加-实际上返回了一个日期,其中添加了一天,因此您必须这样写:

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

以前,这已经咬了我很多次,所以我认为对最糟糕的C#陷阱进行分类将很有用。


157
返回DateTime.Now.AddDays(1);
crashmstr

23
在AFAIK中,内置值类型都是不可变的,至少在该类型包含的任何方法都返回新项目而不是修改现有项目的情况下。至少,我想不出一个没有做到这一点的方法:一切都很好且一致。
乔尔·科洪

6
可变值类型:System.Collections.Generics.List.Enumerator:((是的,如果您努力尝试,您会发现它的行为很奇怪。)
Jon Skeet

13
智能感知为您提供所需的所有信息。它说它返回一个DateTime对象。如果它只是改变了您传入的内容,那将是一个无效方法。
约翰·卡夫

20
不一定:例如,StringBuilder.Append(...)返回“ this”。在流畅的界面中这很普遍。
乔恩·斯基特

Answers:


304
private int myVar;
public int MyVar
{
    get { return MyVar; }
}

布拉莫 您的应用崩溃,没有堆栈跟踪。一直发生。

(注意使用大写MyVar字母而不是小写字母myVar)。


112
和适用于该网站的信息:)
gbjbaanb'

62
我在私人成员上加了下划线,有很大帮助!
chakrit

61
我在可能的地方使用自动属性,很多时候可以停止这种问题;)
TWith2Sugars

28
这是在您的私有字段中使用前缀的绝佳原因(有其他前缀,但这是个好习惯):_myVar,m_myVar
jrista

205
@jrista:o请NO ...不是... M_ AARGH恐怖...
fretje

254

Type.GetType

我看到很多人咬过的是Type.GetType(string)。他们想知道为什么它适用于他们自己的程序集中的类型,而某些类型像System.String,而不是System.Windows.Forms.Form。答案是,它仅在当前程序集中和中查找mscorlib


匿名方法

C#2.0引入了匿名方法,导致了如下讨厌的情况:

using System;
using System.Threading;

class Test
{
    static void Main()
    {
        for (int i=0; i < 10; i++)
        {
            ThreadStart ts = delegate { Console.WriteLine(i); };
            new Thread(ts).Start();
        }
    }
}

那会打印出什么?好吧,这完全取决于调度。它会打印10个数字,但可能不会打印0、1、2、3、4、5、6、7、8、9,这可能是您期望的。问题在于i捕获的变量是变量,而不是在创建委托时的值。使用正确范围的额外局部变量可以轻松解决此问题:

using System;
using System.Threading;

class Test
{
    static void Main()
    {
        for (int i=0; i < 10; i++)
        {
            int copy = i;
            ThreadStart ts = delegate { Console.WriteLine(copy); };
            new Thread(ts).Start();
        }
    }
}

推迟执行迭代器块

这个“穷人的单元测试”没有通过-为什么不呢?

using System;
using System.Collections.Generic;
using System.Diagnostics;

class Test
{
    static IEnumerable<char> CapitalLetters(string input)
    {
        if (input == null)
        {
            throw new ArgumentNullException(input);
        }
        foreach (char c in input)
        {
            yield return char.ToUpper(c);
        }
    }

    static void Main()
    {
        // Test that null input is handled correctly
        try
        {
            CapitalLetters(null);
            Console.WriteLine("An exception should have been thrown!");
        }
        catch (ArgumentNullException)
        {
            // Expected
        }
    }
}

答案是,CapitalLetters直到MoveNext()首次调用迭代器的方法时,代码源中的代码才会执行。

我的脑筋急转弯页面上还有其他怪异之处。


25
迭代器的例子很狡猾!
吉米

8
为什么不将其分为3个答案,以便我们可以对每个答案投票,而不是对所有答案投票?
chakrit

13
@chakrit:回想起来,这可能是个好主意,但我认为现在为时已晚。好像我也想争取更多代表...
Jon Skeet

19
实际上,如果您提供AssemblyQualifiedName,则Type.GetType可以工作。Type.GetType(“ System.ServiceModel.EndpointNotFoundException,System.ServiceModel,Version = 3.0.0.0,Culture = neutral,PublicKeyToken = b77a5c561934e089”);
chilltemp

2
@kentaromiura:重载解析从最派生的类型开始,并处理树-但仅查看最初在它所查看的类型中声明的方法。Foo(int)覆盖了基本方法,因此不考虑。Foo(object)是适用的,因此重载解析在那里停止。奇怪,我知道。
乔恩·斯基特

194

重新抛出异常

重新抛出异常语义是获得许多新开发人员的陷阱。

很多时候我看到如下代码

catch(Exception e) 
{
   // Do stuff 
   throw e; 
}

问题在于,它会擦除​​堆栈跟踪,并使诊断问题变得更加困难,导致您无法跟踪异常的起源。

正确的代码是不带参数的throw语句:

catch(Exception)
{
    throw;
}

或者将异常包装在另一个异常中,然后使用内部异常来获取原始堆栈跟踪:

catch(Exception e) 
{
   // Do stuff 
   throw new MySpecialException(e); 
}

幸运的是,我在第一周就得到了某人的教taught,并且可以在更高级的开发人员代码中找到它。是:catch(){throw; 与第二个代码段相同吗?catch(Exception e){throw; 仅它不会创建Exception对象并填充它?
StuperUser

除了使用throw ex(或throw e)而不是throw的错误外,我还想知道在什么情况下值得捕获异常而仅再次抛出异常。
Ryan Lundy

13
@Kyralessa:有很多情况:例如,如果要回滚事务,则在调用者获得异常之前。您回滚,然后重新抛出。
R. Martinho Fernandes

7
我一直在看到人们捕获并抛出异常的原因,只是因为他们被告知必须捕获所有异常,而没有意识到将在调用堆栈中进一步捕获异常。它使我发疯。
詹姆斯·韦斯特盖特

5
@Kyralessa最大的情况是必须记录日志。在catch中记录错误,然后重新抛出
。.– nawfal

194

海森堡观察窗

如果您正在执行按需加载的东西,这可能会严重地咬住您,例如:

private MyClass _myObj;
public MyClass MyObj {
  get {
    if (_myObj == null)
      _myObj = CreateMyObj(); // some other code to create my object
    return _myObj;
  }
}

现在,假设您在其他地方使用此代码:

// blah
// blah
MyObj.DoStuff(); // Line 3
// blah

现在您要调试CreateMyObj()方法。因此,您在上面的第3行上放置了一个断点,以打算进入代码。出于很好的考虑,您还在上面的行上设置了一个断点_myObj = CreateMyObj();,甚至在CreateMyObj()其内部设置了一个断点。

该代码到达第3行上的断点。您进入该代码。您希望输入条件代码,因为_myObj显然为null,对吗?嗯...所以...为什么跳过条件直接进入return _myObj?您将鼠标悬停在_myObj上……的确,它确实具有价值!那是怎么发生的?!

答案是您的IDE导致它获得了一个值,因为您打开了一个“监视”窗口-尤其是“自动”监视窗口,其中显示了与当前或上一行执行相关的所有变量/属性的值。当你打你的断点在第3行,监视窗口中决定,你有兴趣知道的价值MyObj-所以在幕后,忽略任何断点的,它去和计算的价值MyObj为你- 包括调用CreateMyObj()该设置_myObj的值!

这就是为什么我将其称为“海森堡监视窗口”的原因-您不能在不影响它的情况下观察该值... :)

GOTCHA!


编辑 -我认为@ChristianHayter的评论应包含在主要答案中,因为这似乎是解决此问题的有效方法。因此,只要您拥有延迟加载的属性...

用[DebuggerBrowsable(DebuggerBrowsableState.Never)]或[DebuggerDisplay(“ <按需加载>>”)]装饰属性。–克里斯蒂安·海特(Christian Hayter)


10
辉煌的发现!您不是程序员,而是真正的调试器。
这个。__curious_geek

26
我碰到过这个问题,甚至将鼠标悬停在变量上,而不仅仅是在监视窗口上。
理查德·摩根

31
[DebuggerBrowsable(DebuggerBrowsableState.Never)]或装饰您的财产[DebuggerDisplay("<loaded on demand>")]
Christian Hayter

4
如果您正在开发框架类并且希望监视窗口功能而不改变惰性构造属性的运行时行为,则可以使用调试器类型代理返回值(如果已构造该值)以及一条消息,指出该属性尚未构造是这样的话。Lazy<T>该类(尤其是其Value属性)是使用该类的一个示例。
Sam Harwell

4
我记得有人(由于某种原因我无法理解)以的重载更改了对象的值ToString。每次他将鼠标悬停在它上面时,工具提示都为他提供了不同的价值-他无法弄清楚……
JNF

144

这是另一个让我着迷的时间:

static void PrintHowLong(DateTime a, DateTime b)
{
    TimeSpan span = a - b;
    Console.WriteLine(span.Seconds);        // WRONG!
    Console.WriteLine(span.TotalSeconds);   // RIGHT!
}

TimeSpan.Seconds是时间跨度的秒部分(2分钟和0秒的秒值为0)。

TimeSpan.TotalSeconds是以秒为单位测量的整个时间跨度(2分钟的总秒数为120)。


1
是的,那个也让我知道。我认为应该是TimeSpan.SecondsPart或其他可以使它更清楚表示什么的东西。
丹·迪普洛

3
在重新阅读此内容时,我想知道为什么TimeSpan甚至没有Seconds财产。谁给老鼠的屁股时间的秒数是多少?这是一个任意的,与单位有关的值;我无法想到任何实际用途。
MusiGenesis

2
在我看来,TimeSpan.TotalSeconds将返回...时间跨度中的总秒数。
Ed S.

16
@MusiGenesis该属性很有用。如果要显示分段的时间跨度怎么办?例如,假设您的时间跨度表示“ 3小时15分10秒”的持续时间。没有秒,小时,分钟属性,您如何访问此信息?
SolutionYogi

1
在类似的API中,我使用SecondsPartSecondsTotal区分了两者。
BlueRaja-Danny Pflughoeft 2015年

80

内存泄漏,因为您没有取消挂钩事件。

这甚至吸引了一些我认识的高级开发人员。

想象一下一个WPF表单,其中包含许多内容,并且您在其中订阅了一个事件。如果您不取消订阅,则在关闭并取消引用后,整个表单将保留在内存中。

我相信我看到的问题是在WPF表单中创建DispatchTimer并订阅Tick事件,如果您不对计时器执行-=则您的表单会泄漏内存!

在此示例中,您的拆卸代码应具有

timer.Tick -= TimerTickEventHandler;

由于您在WPF表单内创建了DispatchTimer的实例,因此这一点特别棘手,因此您会认为这将是垃圾回收过程处理的内部引用...不幸的是DispatchTimer使用了订阅和服务的静态内部列表。在UI线程上请求,因此引用由静态类“拥有”。


1
诀窍是始终释放您创建的所有事件订阅。如果您开始依靠Forms为您完成这项工作,则可以确保您会养成习惯,并且有一天会忘记在需要完成的地方发布事件。
杰森·威廉姆斯

3
没有为弱引用事件的MS-CONNECT建议在这里这将解决这个问题,但在我看来,我们应该完全有弱耦合的一个替换事件模型难以置信的差,就像用CAB使用。
BlueRaja-Danny Pflughoeft

向我+1,谢谢!好吧,谢谢我要做的代码审查工作!
鲍勃·丹尼

@ BlueRaja-DannyPflughoeft在弱事件中,您还有另一个陷阱-您无法订阅lambda。您不能写timer.Tick += (s, e,) => { Console.WriteLine(s); }
Ark-kun

@ Ark-kun是的,lambda使它变得更加困难,您必须将lambda保存为变量,并在拆卸代码中使用它。Kinda破坏了编写lambda的简单性,不是吗?
蒂莫西·沃尔特斯

63

也许不是真正的陷阱,因为该行为是用MSDN清楚地编写的,但因为我发现它与直觉相反而使我不知所措:

Image image = System.Drawing.Image.FromFile("nice.pic");

这个家伙将"nice.pic"文件锁定,直到处理完图像为止。当时,我遇到了麻烦,但是尽管可以动态地加载图标会很不错,但一开始并没有意识到我最终得到了数十个打开和锁定的文件!图片跟踪其从中加载文件的位置...

如何解决呢?我以为只有一支班轮能胜任这项工作。我期望有一个额外的参数FromFile(),但没有,所以我写了这个...

using (Stream fs = new FileStream("nice.pic", FileMode.Open, FileAccess.Read))
{
    image = System.Drawing.Image.FromStream(fs);
}

10
我同意这种行为是没有道理的。除了“此行为是设计使然”之外,我找不到任何解释。
MusiGenesis

1
哦,这个解决方法的最大好处是,如果您稍后尝试调用Image.ToStream(我忘记了确切的名称),它将无法正常工作。
约书亚

55
需要检查一些代码。b
Esben Skov Pedersen 2012年

7
@EsbenSkovPedersen这样简单但有趣又枯燥的评论。让我开心。
Inisheer 2013年

51

如果您使用ASP.NET,我想说Webforms生命周期对我来说是一个很大的难题。我花了无数小时来调试写得不好的Web表单代码,这是因为许多开发人员实际上并不真正了解何时使用哪个事件处理程序(不幸的是,包括我在内)。


26
这就是为什么我搬到MVC的原因……viewstate头疼……
chakrit

29
还有一个专门针对ASP.NET陷阱的其他问题(理应如此)。ASP.NET的基本概念(使Web应用程序对开发人员来说就像Windows应用程序一样)太可怕了,以至于我不确定它甚至算不上“陷阱”。
MusiGenesis

1
我希望我能对你的评论投一百次。
csauve

3
@MusiGenesis现在看来似乎有些误导,但当时人们希望其Web应用程序(应用程序是关键词-ASP.NET WebForms并非真正旨在托管博客)的行为与Windows应用程序相同。这只是最近才改变的,很多人仍然“不在那儿”。整个问题是抽象的方法太漏了-Web的行为不像桌面应用程序那么多,它几乎导致所有人的困惑。
2014年

1
具有讽刺意味的是,我所见到的关于ASP.NET的第一件事是来自Microsoft的视频,演示了如何轻松地使用ASP.NET创建博客站点!
MusiGenesis

51

重载==运算符和未类型化的容器(数组列表,数据集等):

string my = "my ";
Debug.Assert(my+"string" == "my string"); //true

var a = new ArrayList();
a.Add(my+"string");
a.Add("my string");

// uses ==(object) instead of ==(string)
Debug.Assert(a[1] == "my string"); // true, due to interning magic
Debug.Assert(a[0] == "my string"); // false

解决方案?

  • string.Equals(a, b)比较字符串类型时始终使用

  • 使用泛型就像List<string>确保两个操作数都是字符串一样。


6
您在其中有多余的空格,这会使所有操作都出错-但是,如果您删除空格,则最后一行仍然是正确的,因为“ my” +“ string”仍然是常量。
乔恩·斯基特

1
ack!你是对的:)好的,我编辑了一下。
吉米

在此类用途上会产生警告。
chakrit

11
是的,C#语言的最大缺陷之一是Object类中的==运算符。他们应该强迫我们使用ReferenceEquals。
erikkallen's

2
幸运的是,从2.0开始,我们有了泛型。如果您在上面的示例中使用List <string>而不是ArrayList,则无需担心。另外,我们已经从中获得了性能,是的!我总是在我们的旧代码中根除对ArrayList的旧引用。
JoelC,2014年

48
[Serializable]
class Hello
{
    readonly object accountsLock = new object();
}

//Do stuff to deserialize Hello with BinaryFormatter
//and now... accountsLock == null ;)

故事的寓意:反序列化对象时不运行字段初始化程序


8
是的,我讨厌.NET序列化未运行默认构造函数。我希望没有调用任何构造函数就不可能构造一个对象,但是事实并非如此。
罗曼·斯塔科夫

45

DateTime.ToString(“ dd / MM / yyyy”) ; 实际上,这并不总是为您提供dd / MM / yyyy,而是考虑区域设置并根据您所在的位置替换日期分隔符。因此,您可能会得到dd-MM-yyyy或类似内容。

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


DateTime.ToString(“ r”)应该转换为使用GMT的RFC1123。GMT距UTC不到一秒钟,但是即使有问题的DateTime指定为Local ,“ r”格式说明符也不会转换为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
@Beska:因为您正在写入文件,所以它必须是具有指定日期格式的特定格式。
GvS

11
我认为本地化的默认值比反之更差。至少有一部分开发人员完全忽略了本地化,因此代码在不同本地化的机器上工作。这样,代码可能无法正常工作。
约书亚

32
实际上,我相信这样做的正确方法是DateTime.ToString("dd/MM/yyyy", CultureInfo.InvariantCulture);
BlueRaja-Danny Pflughoeft 2010年

44

我前几天看到这则贴子,我觉得它晦涩难懂,对那些不知道的人来说很痛苦

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

因为这将返回0,而不是大多数人期望的1


37
我希望那实际上不会咬人-我真的希望他们不会首先写它!(当然,这很有趣。)
乔恩·斯凯特

12
我认为这不是很晦涩...
克里斯·马拉斯蒂·乔治

10
至少在C#中,如果意外,则定义结果。在C ++中,它可能是0或1,或者包括程序终止在内的任何其他结果!
James Curran

7
这不是陷阱。x = x ++-> x = x,然后递增x .... x = ++ x->递增x然后x = x
凯文,

28
@Kevin:我认为这不是那么简单。如果x = x ++等于x = x,然后是x ++,那么结果将是x =1。相反,我认为发生的事情是首先对等号右边的表达式求值(给出0),然后x是递增(给定x = 1),最后执行分配(再次给定x = 0)。
Tim Goodman 2010年

39

我参加这个聚会有点晚了,但是最近有两个我都被我咬的陷阱:

日期时间分辨率

“ Ticks”属性的测量时间为百万分之一秒(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.625ms的舍入块形式获取值:15、31、46等。

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


路径组合

组合文件路径的好方法,但并不总是能达到您期望的方式。

如果第二个参数以\字符开头,则不会给出完整的路径:

这段代码:

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
在〜15ms间隔内对时间进行量化并不是因为底层计时机制缺乏准确性(我忽略了前面的阐述)。这是因为您的应用程序在多任务操作系统中运行。Windows每隔15毫秒左右检查一次您的应用程序,并且在它获得的很少时间片中,您的应用程序处理自上一个片段以来排队的所有消息。您在该分片中的所有调用均会返回完全相同的时间,因为它们实际上都是在完全相同的时间进行的。
MusiGenesis

2
@MusiGenesis:我知道(现在)它是如何工作的,但是对我来说,如此精确的测量方法似乎并没有那么精确,这似乎误导了我。这就像说我知道我的身高(以纳米为单位)时,实际上我将其四舍五入到最接近的一千万。
Damovisa

7
DateTime能够存储最多一个刻度。现在是DateTime。现在没有使用这种精度。
鲁宾

16
额外的'\'是许多unix / mac / linux用户的陷阱。在Windows中,如果有一个前导“ \”,则表示我们要进入驱动器的根目录(即C :),在CD命令中进行尝试以了解我的意思...。1)转到C:\Windows\System322)输入CD \Users3)哇!现在您在C:\Users...了解了吗?... Path.Combine(@“ C:\ Windows \ System32”,@“ \ Users”)应该返回\Users,这意味着[current_drive_here]:\Users
–chkrit

8
即使没有“睡眠”,其执行方式也相同。这与每15毫秒安排一次的应用程序无关。DateTime.UtcNow调用的本机函数GetSystemTimeAsFileTime似乎分辨率较差。
Jimbo

38

当您启动一个过程(使用System.Diagnostics)并写入控制台时,但您从未读过Console.Out流,经过一定量的输出后,您的应用程序似乎会挂起。


3
当您同时重定向stdout和stderr并依次使用两个ReadToEnd调用时,仍然可能发生相同的情况。为了安全处理stdout和stderr,您必须为它们各自创建一个读取线程。
塞巴斯蒂安M

34

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

这里

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


8
瓷砖。BRB,重新优化了数百个LINQ查询...
tsilb

30

在虚拟方法中使用默认参数

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 2014年

1
@staafl默认参数在编译时而不是运行时解析。
fredoverflow 2014年

1
我会说这个陷阱通常是默认参数-人们通常不会意识到它们是在编译时而不是运行时解决的。
2014年

4
@FredOverflow,我的问题是概念性的。尽管该行为对于实现而言是有意义的,但这是不直观的,并且可能是错误的来源。恕我直言,C#编译器在覆盖时不应允许更改默认参数值。
staafl 2014年

27

值可变集合中的对象

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>),因此我可以看到这令人困惑。
拉瑟五世卡尔森

2
你是对的。谢谢。我确定了答案:)。arr[i].attr=是无法在库容器中编写代码的数组的特殊语法;(。为什么(<value expression>)。attr = <expr>完全允许?它是否有意义?
Bjarke Ebert,

1
@Bjarke Ebert:在某些情况下,这是有道理的,但不幸的是,编译器无法识别并允许它们。示例使用场景:一个不可变的Struct,其中包含对方形二维数组的引用以及“旋转/翻转”指示器。该结构本身将是不可变的,因此写入只读实例的元素应该可以,但是编译器不会知道该属性设置程序实际上不会编写该结构,因此将不允许它。
2012年

25

也许不是最坏的情况,但是.net框架的某些部分使用度,而其他部分使用弧度 (并且Intellisense附带的文档从不告诉您哪个,您必须访问MSDN才能找到答案)

所有这些都可以通过上一Angle堂课来避免...


令我惊讶的是,它获得了如此多的赞誉,考虑到我的其他陷阱比这严重得多
BlueRaja-Danny Pflughoeft 2010年

22

对于C / C ++程序员来说,自然而然地过渡到C#。但是,我个人遇到的最大难题(并已经看到与其他人进行相同的转换)并不能完全理解C#中的类和结构之间的区别。

在C ++中,类和结构是相同的。它们仅在默认可见性方面有所不同,其中类默认为私有可见性,而结构默认为公共可见性。在C ++中,此类定义

    class A
    {
    public:
        int i;
    };

在功能上等同于此struct定义。

    struct A
    {
        int i;
    };

但是,在C#中,类是引用类型,而结构是值类型。这使得一个的(1)决定何时使用了另一种,(2)测试对象的相等,(3)的性能(例如,装箱/拆箱)等差

网上有各种各样与两者之间的差异有关的信息(例如here)。我强烈鼓励任何向C#过渡的人至少对差异及其含义有一定的了解。


13
那么,最糟糕的陷阱是人们在使用它之前不花时间去学习这种语言吗?
BlueRaja-Danny Pflughoeft

3
@ BlueRaja-DannyPflughoeft更像是表面上相似的语言的经典技巧-它们使用非常相似的关键字,并且在许多情况下使用语法,但是它们的工作方式却大不相同。
a安

19

垃圾回收和Dispose()。尽管您无需执行任何操作来释放内存,但仍然必须通过Dispose()释放资源。当您使用WinForms或以任何方式跟踪对象时,这是一件非常容易忘记的事情。


2
using()块巧妙地解决了这个问题。每当看到对Dispose的调用时,都可以立即安全地重构为使用using()。
杰里米·弗雷

5
我认为关注点在于正确实现 IDisposable。
Mark Brackett

4
另一方面,using()习惯可能会意外地咬你,就像使用PInvoke一样。您不想处理API仍在引用的内容。
MusiGenesis

3
正确地实现IDisposable很难,而且即使我最终(“ .NET Framework准则”)找到的最佳建议也很难理解,直到最终“理解”它。
Quibblesome

1
我在IDisposable上找到的最佳建议来自Stephen Cleary,其中包括三个简单的规则以及有关IDisposable的深入文章
Roman Starkov 2011年

19

数组工具 IList

但是不要实施它。当您致电添加时,它告诉您它不起作用。那么,为什么一个类在无法支持该接口的情况下实现该接口呢?

编译但不起作用:

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

我们有很多这个问题,因为串行器(WCF)将所有IList转换为数组,并且会出现运行时错误。


8
恕我直言,问题在于Microsoft没有为集合定义足够的接口。恕我直言,它应该具有iEnumerable,iMultipassEnumerable(支持Reset,并确保多次传递将匹配),iLiveEnumerable(如果集合在枚举过程中发生更改,则将具有部分定义的语义-更改可能会或可能不会出现在枚举中,但不应引起更改)虚假结果或异常),iReadIndexable,iReadWriteIndexable等。由于接口可以“继承”其他接口,因此不会增加任何额外的工作(如果有的话)(它将节省NotImplemented存根)。
supercat

@supercat,对于初学者和某些长期的编码人员来说,真是令人困惑。我认为.NET集合及其界面非常优雅。但我感谢您的谦卑。;)
乔丹(Jordan

@Jordan:自从编写以上内容以来,我已经决定一种更好的方法是同时拥有IEnumerable<T>IEnumerator<T>支持一个Features属性以及一些“可选”方法,这些方法的有用性取决于“功能”的报告内容。不过,我坚持我的主要观点,那就是在某些情况下,接收IEnumerable<T>遗嘱的代码需要比IEnumerable<T>提供的承诺更严格的承诺。调用ToList将产生一个IEnumerable<T>支持此类承诺的方法,但在许多情况下会不必要地增加成本。我认为应该有……
超级猫

...一种手段,使接收代码的人IEnumerable<T>可以在需要时复制内容但可以避免这样做。
2015年

您的选择绝对不可读。当我在代码中看到一个IList时,我知道我正在使用什么,而不必探究Features属性。程序员喜欢忘记代码的一个重要特征是它不仅可以被计算机读取,还可以被人们读取。.NET集合名称空间不是理想的,但它是好的,有时找到最佳解决方案并不是更理想地适合原则的问题。我曾经处理过的最糟糕的代码是试图完美地适合DRY的代码。我把它报废了并改写了。那只是不好的代码。我根本不想使用您的框架。
约旦,

18

foreach循环变量作用域!

var l = new List<Func<string>>();
var strings = new[] { "Lorem" , "ipsum", "dolor", "sit", "amet" };
foreach (var s in strings)
{
    l.Add(() => s);
}

foreach (var a in l)
    Console.WriteLine(a());

打印五个“ amet”,而以下示例可以正常工作

var l = new List<Func<string>>();
var strings = new[] { "Lorem" , "ipsum", "dolor", "sit", "amet" };
foreach (var s in strings)
{
    var t = s;
    l.Add(() => t);
}

foreach (var a in l)
    Console.WriteLine(a());

11
这本质上等同于乔恩使用匿名方法的示例。
Mehrdad Afshari,2009年

3
除非它与foreach混淆,否则“ s”变量更容易与作用域变量混合。使用通用的for循环,每次迭代的索引变量显然是相同的。
Mikko Rantanen

2
blogs.msdn.com/ericlippert/archive/2009/11/12/…是的,希望该变量的作用域为“正确”。
罗曼·斯塔科夫


实际上,您只是一次又一次地打印相同的变量而无需更改它。
约旦

18

MS SQL Server无法处理1753年之前的日期。重要的是,这与.NET DateTime.MinDate常数1/1/1 不同步。因此,如果您试图节省思想,一个格式错误的日期(最近一次在数据导入中发生在我身上)或仅仅是征服者威廉的出生日期,那您就会遇到麻烦了。没有内置的解决方法。如果您可能需要处理1753年之前的日期,则需要编写自己的解决方法。


17
坦率地说,我认为MS SQL Server具有此权利,.Net是错误的。如果您进行研究,那么您会知道1751年之前的日期会由于日历更改,完全跳过的天数等而变得时髦。大多数RDBM都有一些截止点。这应该给您一个起点:ancestry.com/learn/library/article.aspx?
article=3358

11
另外,日期是1753。这几乎是我们第一次拥有连续日历而不跳过日期。SQL 2008引入了Date和datetime2日期类型,可以接受从1/1/01到12/31/9999的日期。但是,如果您确实在比较1753年之前的日期,则应该怀疑使用这些类型的日期比较。
2009年

哦,对,1753年,纠正了,谢谢。
Shaul Behr,2009年

将日期与此类日期进行比较真的有意义吗?我的意思是,对于历史频道来说,这很有道理,但是我不希望自己知道美国被发现的确切时间。
卡米洛·马丁

5
通过儒略日的Wikipedia,您可以找到一个13行的基本程序CALJD.BAS,该程序于1984年发布,可以考虑到leap日和1753年的跳过天,计算日期可以追溯到公元前5000年。所以我不明白为什么“现代像SQL2008这样的系统应该会更糟。您可能对15世纪的正确日期表示形式不感兴趣,但是其他人可能对此感兴趣,并且我们的软件应能够毫无错误地进行处理。另一个问题是leap秒。。。
罗兰

18

讨厌的Linq缓存Gotcha

请参阅导致该发现的我的问题以及发现该问题的博客

简而言之,DataContext保留了您曾经加载的所有Linq-to-Sql对象的缓存。如果其他任何人对您先前加载的记录进行了任何更改,即使您明确地重新加载了该记录,也将无法获取最新数据

这是因为ObjectTrackingEnabled在DataContext上调用了一个属性,该属性默认为true。如果将该属性设置为false,则每次都会重新加载记录... 但是 ...您无法使用SubmitChanges()持久保存对该记录的任何更改。

GOTCHA!


四只花了一天半的时间追逐这个BUG ...(和发负荷!)
手术编码器

这被称为并发冲突,尽管尽管现在有一些解决方法,尽管它们似乎有些繁重,但今天仍然是一个难题。DataContext是一场噩梦。O_o
约旦,

17

我看到Stream.Read上的合同吸引了很多人:

// Read 8 bytes and turn them into a ulong
byte[] data = new byte[8];
stream.Read(data, 0, 8); // <-- WRONG!
ulong data = BitConverter.ToUInt64(data);

这是错误的原因是,Stream.Read它将最多读取指定的字节数,但是即使在流结束之前还有7个字节可用,也可以完全自由地仅读取1个字节。

它看起来像Stream.Write,这无济于事,如果返回无异常,保证已写入所有字节。上面的代码几乎一直都起作用也无济于事。当然,没有现成的,方便的方法来正确地正确读取N个字节也无济于事。

因此,要塞住漏洞并提高对此的认识,下面是一个正确的方法示例:

    /// <summary>
    /// Attempts to fill the buffer with the specified number of bytes from the
    /// stream. If there are fewer bytes left in the stream than requested then
    /// all available bytes will be read into the buffer.
    /// </summary>
    /// <param name="stream">Stream to read from.</param>
    /// <param name="buffer">Buffer to write the bytes to.</param>
    /// <param name="offset">Offset at which to write the first byte read from
    ///                      the stream.</param>
    /// <param name="length">Number of bytes to read from the stream.</param>
    /// <returns>Number of bytes read from the stream into buffer. This may be
    ///          less than requested, but only if the stream ended before the
    ///          required number of bytes were read.</returns>
    public static int FillBuffer(this Stream stream,
                                 byte[] buffer, int offset, int length)
    {
        int totalRead = 0;
        while (length > 0)
        {
            var read = stream.Read(buffer, offset, length);
            if (read == 0)
                return totalRead;
            offset += read;
            length -= read;
            totalRead += read;
        }
        return totalRead;
    }

    /// <summary>
    /// Attempts to read the specified number of bytes from the stream. If
    /// there are fewer bytes left before the end of the stream, a shorter
    /// (possibly empty) array is returned.
    /// </summary>
    /// <param name="stream">Stream to read from.</param>
    /// <param name="length">Number of bytes to read from the stream.</param>
    public static byte[] Read(this Stream stream, int length)
    {
        byte[] buf = new byte[length];
        int read = stream.FillBuffer(buf, 0, length);
        if (read < length)
            Array.Resize(ref buf, read);
        return buf;
    }

1
或者,在您的明确示例中:var r = new BinaryReader(stream); ulong data = r.ReadUInt64();。BinaryReader也有一种FillBuffer方法...
jimbobmcgee 2014年

15

大事记

我从来不明白为什么事件是一种语言功能。它们使用起来很复杂:您需要在调用之前检查null,需要(自己)注销,您无法找到谁进行了注册(例如:我注册了吗?)。为什么事件不只是图书馆中的一门课?基本上是专业的List<delegate>


1
而且,多线程很痛苦。所有这些问题(空值都已在CAB中修复)(其功能实际上应该内置于语言中)-事件是全局声明的,任何方法都可以将自身声明为任何事件的“订阅者”。我与CAB的唯一问题是,全局事件名称是字符串而不是枚举(可以通过更智能的枚举来固定,例如Java,它固有地充当字符串!)。CAB是难以成立的,但有一个简单的开源克隆可在这里
BlueRaja-Danny Pflughoeft,2010年

3
我不喜欢.net事件的实现。事件订阅应通过调用添加订阅并返回IDisposable的方法来处理,该方法在Dispose时将删除订阅。不需要将“ add”和“ remove”方法组合在一起的特殊结构,其语义可能有些晦涩,特别是如果有人尝试添加并随后删除多播委托(例如,先添加“ B”再加上“ AB”,然后删除) “ B”(离开“ BA”)和“ AB”(仍然离开“ BA”)。糟糕
supercat

@supercat您将如何重写button.Click += (s, e) => { Console.WriteLine(s); }
方舟坤

如果我必须能够与其他事件分开IEventSubscription clickSubscription = button.SubscribeClick((s,e)=>{Console.WriteLine(s);});退订,并通过退订clickSubscription.Dispose();。如果我的对象将在整个生命周期中保留所有订阅,MySubscriptions.Add(button.SubscribeClick((s,e)=>{Console.WriteLine(s);}));然后MySubscriptions.Dispose()杀死所有订阅。
2013年

@ Ark-kun:必须保留封装外部订阅的对象似乎很麻烦,但是将订阅视为实体可以将它们聚合为可以确保将其全部清除的类型,否则这将非常困难。
2013年

14

今天,我修复了一个长时间未发现的错误。该错误位于用于多线程方案的通用类中,并且使用静态int字段使用Interlocked提供无锁同步。引起该错误的原因是,类型的泛型类的每个实例化都有其自己的静态变量。因此,每个线程都有其自己的静态字段,并且未按预期使用锁。

class SomeGeneric<T>
{
    public static int i = 0;
}

class Test
{
    public static void main(string[] args)
    {
        SomeGeneric<int>.i = 5;
        SomeGeneric<string>.i = 10;
        Console.WriteLine(SomeGeneric<int>.i);
        Console.WriteLine(SomeGeneric<string>.i);
        Console.WriteLine(SomeGeneric<int>.i);
    }
}

打印5 10 5


5
您可以有一个非泛型基类,该基类定义静态变量,并从中继承泛型。尽管我从来没有因为C#的这种行为而死-我仍然记得某些C ++模板的调试时间很长...哎呀!:)
Paulius

7
很奇怪,我认为这很明显。只要考虑一下如果i有类型该怎么办T
Timwi

1
类型参数是的一部分TypeSomeGeneric<int>与类型不同SomeGeneric<string>; 当然,这样每个人都有自己public static int i
radarbob

13

可枚举可被多次评估

当您有一个惰性枚举的枚举,并且对其进行两次迭代并获得不同的结果时,它会咬您。(或者您得到相同的结果,但不必要地执行两次)

例如,在编写特定测试时,我需要一些临时文件来测试逻辑:

var files = Enumerable.Range(0, 5)
    .Select(i => Path.GetTempFileName());

foreach (var file in files)
    File.WriteAllText(file, "HELLO WORLD!");

/* ... many lines of codes later ... */

foreach (var file in files)
    File.Delete(file);

想象一下当我File.Delete(file)抛出时的惊喜FileNotFound

这里发生的是,该files枚举对象被迭代了两次(第一次迭代的结果根本不被记住),并且在每个新的迭代中您都将被调用,Path.GetTempFilename()因此您将获得一组不同的临时文件名。

解决方案当然是使用ToArray()或渴望枚举值ToList()

var files = Enumerable.Range(0, 5)
    .Select(i => Path.GetTempFileName())
    .ToArray();

当您执行多线程操作时,这甚至更可怕:

foreach (var file in files)
    content = content + File.ReadAllText(file);

content.Length在所有写入之后,您仍然发现其为0!然后,您开始严格检查以下情况:....一个小时的浪费之后...您发现这只是您忘记的那小小的可枚举陷阱...。


这是设计使然。这称为延迟执行。除其他外,它旨在模拟TSQL构造。每次从sql视图中选择时,都会得到不同的结果。它还允许链接,这对于远程数据存储(例如SQL Server)很有用。否则x.Select.Where.OrderBy将向数据库发送3个单独的命令...
as9876 2015年

@AYS您是否错过了问题标题中的“ Gotcha”一词?
chakrit

我以为陷阱意味着对设计师的监督,而不是故意的。
as9876

对于不可重新启动的IEnumerable,也许应该有另一种类型。像AutoBufferedEnumerable吗?一个人可以轻松实现它。这个陷阱似乎主要是由于程序员缺乏知识,我认为当前行为没有任何问题。
Eldritch难题,2016年

13

刚发现一个奇怪的东西让我陷入了调试一段时间:

您可以为可为null的int增加null而不抛出异常,并且该值保持为null。

int? i = null;
i++; // I would have expected an exception but runs fine and stays as null

这是C#如何利用可空类型的操作的结果。这有点类似于NaN消耗掉您投入的所有内容。
IllidanS4希望莫妮卡回到

10
TextInfo textInfo = Thread.CurrentThread.CurrentCulture.TextInfo;

textInfo.ToTitleCase("hello world!"); //Returns "Hello World!"
textInfo.ToTitleCase("hElLo WoRld!"); //Returns "Hello World!"
textInfo.ToTitleCase("Hello World!"); //Returns "Hello World!"
textInfo.ToTitleCase("HELLO WORLD!"); //Returns "HELLO WORLD!"

是的,该行为已记录在案,但这肯定是不正确的。


5
我不同意-当大写一个单词时,它可能具有特殊含义,即您不想弄乱标题案例,例如“美国总统”->“美国总统”,而不是“美国总统”美国”。
Shaul Behr,2010年

5
@Shaul:在这种情况下,他们应该将其指定为一个参数,以避免混淆,因为我从来没有见过任何人谁提前预计的时间这种行为-这使它成为一个疑难杂症
BlueRaja-Danny Pflughoeft 2010年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.