什么是C#或.NET中最糟糕的问题?

我最近正在使用一个DateTime对象,并写了这样的东西:

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

AddDays()的intellisense文档说,它增加了一天到date,它不会 – 它实际上返回一个date添加到它的一天,所以你必须写下来:

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

这个人之前已经咬了我好几次了,所以我认为对最糟糕的C#陷阱进行编目是很有用的。

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

Blammo。 你的应用程序崩溃,没有堆栈跟踪。 一直发生。

(注意大写MyVar而不是getter中的小写myVar 。)

Type.GetType

我见过的很多人是Type.GetType(string) 。 他们想知道为什么它适用于自己的程序集中的types,以及一些types如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,这是你所期望的。 问题在于它是被捕获的ivariables,而不是在创build委托时的值。 这可以通过一个额外的右侧范围的局部variables轻松解决:

 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(); } } } 

推迟迭代器块的执行

这个“穷人的unit testing”没有通过 – 为什么不呢?

 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 } } } 

答案是,在首先调用迭代器的MoveNext()方法之前,不会执行CapitalLetters代码源代码中的代码。

我的脑筋急转弯上有一些其他的怪事。

重新抛出exception

获取大量新开发人员的问题是重抛exception语义。

很多时候我看到下面的代码

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

问题是,它抹去堆栈跟踪,使诊断问题更难,因为你不能跟踪exception的起源。

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

 catch(Exception) { throw; } 

或者在另一个包装exception,并使用内部exception来获得原始堆栈跟踪:

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

海森堡观察窗口

如果你正在做按需加载的东西,这可能会严重地影响你:

 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 

现在你想debugging你的CreateMyObj()方法。 所以你在上面的第3行放置了一个断点,意图进入代码。 只是为了更好的衡量,你还可以在上面的那一行放置一个断点,它表示_myObj = CreateMyObj(); ,甚至是CreateMyObj()本身的一个断点。

代码到达第3行的断点。您进入代码。 你期望进入条件代码,因为_myObj显然是空的,对吧? 呃…那么…为什么跳过这个条件,直接return _myObj ? 你把鼠标hover在_myObj上,而且确实有价值! 那是怎么发生的?!

答案是你的IDE使它得到一个值,因为你有一个“监视”窗口打开 – 特别是“汽车”监视窗口,显示所有variables/属性的值与当前或上一行执行相关。 当你在第三行MyObj点时,观察窗口决定你有兴趣知道MyObj的价值 – 所以在幕后, 忽略任何断点 ,它MyObj为你计算MyObj的值 – 包括呼叫CreateMyObj()设置_myObj的值!

这就是为什么我把这称为海森堡观察窗 – 你不能在不影响它的情况下观察它的价值… 🙂

GOTCHA!


编辑 – 我觉得@ ChristianHayter的评论应该包括在主要答案中,因为这看起来像是一个有效的解决方法。 所以每当你有一个懒惰的财产…

用[DebuggerBrowsable(DebuggerBrowsableState.Never)]或[DebuggerDisplay(“”)]装饰你的属性。 – 基督徒Hayter

这是另外一个让我感到满意的东西:

 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)。

内存泄漏,因为你没有解除事件。

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

想象一下,一个WPF表单中有很多东西,并且在那里你订阅一个事件。 如果您不取消订阅,那么在closures和取消引用后,整个表单将保留在内存中。

我相信我看到的问题是在WPF表单中创build一个DispatchTimer,并订阅Tick事件,如果你没有在表单上泄漏内存的时候执行 – =操作。

在这个例子中,你的拆卸代码应该有

 timer.Tick -= TimerTickEventHandler; 

这是一个非常棘手的问题,因为你在WPF表单中创build了DispatchTimer的实例,所以你会认为这将是一个由垃圾收集进程处理的内部引用。不幸的是,DispatchTimer使用一个静态的订阅和服务内部列表请求在UI线程上,所以引用是由静态类“拥有”的。

也许不是一个问题,因为这个行为在MSDN中写得很清楚,但是由于我发现它违反了直觉:

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

这家伙离开"nice.pic"文件locking,直到image processing。 在我面对它的时候,我虽然很快就能够加载图标,并没有意识到(起初)我已经打开了几十个打开和locking的文件! 图片跟踪它从哪里加载文件…

如何解决这个问题? 我以为一class就可以做这个工作。 我期望额外的参数FromFile() ,但没有,所以我写了这个…

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

如果您计算ASP.NET,我会说webforms生命周期对我来说是一个相当大的问题。 我已经花了无数个小时来debugging写得不好的webforms代码,只是因为很多开发人员并不真正了解何时使用哪个事件处理程序(包括我在内)。

重载==操作符和无types的容器(数组列表,数据集等):

 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 

解决scheme?

  • 在比较stringtypes时总是使用string.Equals(a, b)

  • 使用类似List<string>generics来确保两个操作数都是string。

DateTime.ToString(“dd / MM / yyyy”) ; 这实际上并不总是给你dd / MM / yyyy,而是会考虑到区域设置,并根据你的位置更换date分隔符。 所以你可能会得到dd-MM-yyyy或类似的东西。

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


DateTime.ToString(“r”)应该转换为使用GMT的RFC1123。 格林尼治标准时间与UTC不到一秒,但“r”格式说明符不会转换为UTC ,即使该问题的date时间被指定为本地。

这导致下面的问题(取决于你的本地时间与UTC的距离有多远):

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

哎呦!

 [Serializable] class Hello { readonly object accountsLock = new object(); } //Do stuff to deserialize Hello with BinaryFormatter //and now... accountsLock == null ;) 

这个故事的寓意:在反序列化一个对象时,字段初始化器不运行

前几天我看到了这个post,我觉得这很不明显,对那些不了解的人来说也很痛苦

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

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

当您启动一个写入控制台的进程(使用System.Diagnostics),但是您从不读取Console.Outstream时,在一定量的输出之后,您的应用程序将显示为挂起状态。

我对这个派对有点晚了,但是我最近有两个陷入困境的陷阱:

date时间分辨率

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等。

这个特定的行为因系统而异 ,但是在这个date/时间API中还有其他与解决相关的问题 。


Path.Combine

结合文件path的好方法,但并不总是按照您期望的方式运行。

如果第二个参数以\字符开始,它不会给你一个完整的path:

此代码:

 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\ 

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

看到这里 。

简而言之,在Linq-To-Sql查询的条件子句中,不能使用像||这样的条件快捷方式 和&&以避免空引用exception; 即使第一个条件不需要评估第二个条件,Linq-To-Sql也会评估OR或AND运算符的两侧!

使用默认参数与虚拟方法

 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(); 

输出:
派生的基地

在可变集合中的值对象

 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 

也许不是最糟糕的,但.net框架的某些部分使用度,而其他一些使用弧度 (与Intellisense出现的文档永远不会告诉你,你必须访问MSDN找出)

所有这一切都可以通过一个Angle类来避免。

对于C / C ++程序员来说,转换到C#是一个很自然的过程。 然而,我个人遇到的最大问题(并且与其他人进行了相同的转换)并没有完全理解C#中的类和结构之间的区别。

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

  class A { public: int i; }; 

在function上等同于此结构定义。

  struct A { int i; }; 

但在C#中,类是引用types,而结构是值types。 这在(1)决定何时使用一个在另一个上,(2)testing对象相等,(3)performance(例如,装箱/拆箱)等方面产生了巨大的差异。

networking上有各种各样的信息(比如这里 )。 我会极力鼓励任何转向C#的人至less对差异及其影响有所了解。

垃圾收集和处理()。 虽然你不需要做任何事情来释放内存 ,但你仍然需要通过Dispose()来释放资源 。 当您使用WinForms或以任何方式跟踪对象时,这是一件非常容易的事情。

foreach loops variables scope!

 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()); 

prints five "amet", while the following example works fine

 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()); 

MS SQL Server can't handle dates before 1753. Significantly, that is out of synch with the .NET DateTime.MinDate constant, which is 1/1/1. So if you try to save a mindate, a malformed date (as recently happened to me in a data import) or simply the birth date of William the Conqueror, you're gonna be in trouble. There is no built-in workaround for this; if you're likely to need to work with dates before 1753, you need to write your own workaround.

Arrays implement IList

But don't implement it. When you call Add, it tells you that it doesn't work. So why does a class implement an interface when it can't support it?

Compiles, but doesn't work:

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

We have this issue a lot, because the serializer (WCF) turns all the ILists into arrays and we get runtime errors.

The Nasty Linq Caching Gotcha

See my question that led to this discovery, and the blogger who discovered the problem.

In short, the DataContext keeps a cache of all Linq-to-Sql objects that you have ever loaded. If anyone else makes any changes to a record that you have previously loaded, you will not be able to get the latest data, even if you explicitly reload the record!

This is because of a property called ObjectTrackingEnabled on the DataContext, which by default is true. If you set that property to false, the record will be loaded anew every time… BUT … you can't persist any changes to that record with SubmitChanges().

GOTCHA!

The contract on Stream.Read is something that I've seen trip up a lot of people:

 // 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); 

The reason this is wrong is that Stream.Read will read at most the specified number of bytes, but is entirely free to read just 1 byte, even if another 7 bytes are available before end of stream.

It doesn't help that this looks so similar to Stream.Write , which is guaranteed to have written all the bytes if it returns with no exception. It also doesn't help that the above code works almost all the time . And of course it doesn't help that there is no ready-made, convenient method for reading exactly N bytes correctly.

So, to plug the hole, and increase awareness of this, here is an example of a correct way to do this:

  /// <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; } 

活动

I never understood why events are a language feature. They are complicated to use: you need to check for null before calling, you need to unregister (yourself), you can't find out who is registered (eg: did I register?). Why isn't an event just a class in the library? Basically a specialized List<delegate> ?

Enumerables can be evaluated more than once

It'll bite you when you have a lazily-enumerated enumerable and you iterate over it twice and get different results. (or you get the same results but it executes twice unnecessarily)

For example, while writing a certain test, I needed a few temp files to test the logic:

 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); 

Imagine my surprise when File.Delete(file) throws FileNotFound !!

What's happening here is that the files enumerable got iterated twice (the results from the first iteration are simply not remembered) and on each new iteration you'd be re-calling Path.GetTempFilename() so you'll get a different set of temp filenames.

The solution is, of course, to eager-enumerate the value by using ToArray() or ToList() :

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

This is even scarier when you're doing something multi-threaded, like:

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

and you find out content.Length is still 0 after all the writes!! You then begin to rigorously checks that you don't have a race condition when…. after one wasted hour… you figured out it's just that tiny little Enumerable gotcha thing you forgot….

Today I fixed a bug that eluded for long time. The bug was in a generic class that was used in multi threaded scenario and a static int field was used to provide lock free synchronisation using Interlocked. The bug was caused because each instantiation of the generic class for a type has its own static. So each thread got its own static field and it wasn't used a lock as intended.

 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); } } 

This prints 5 10 5

Just found a weird one that had me stuck in debug for a while:

You can increment null for a nullable int without throwing an excecption and the value stays null.

 int? i = null; i++; // I would have expected an exception but runs fine and stays as null 
 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!" 

Yes, this behavior is documented, but that certainly doesn't make it right.