C#(.NET)设计缺陷

时间:2009-01-04 23:29:26

标签: c# .net

一般来说,C#或.NET Framework中存在哪些最大的设计缺陷?

示例:没有非可空字符串类型,您必须在从IDataReader获取值时检查DBNull。

33 个答案:

答案 0 :(得分:72)

  • Reset()上的IEnumerator<T>方法是错误的(对于迭代器块,语言规范甚至要求这会引发异常)
  • 在Eric看来,返回数组的反射方法是a mistake
  • 阵列协方差是并且仍然是一个奇怪的;至少在C#4.0 / .NET 4.0中,这是针对IEnumerable[<T>]
  • 正确完成的
  • ApplicationException而不是失宠 - 这是一个错误吗?
  • 同步集合 - 一个不错的主意,但在现实中并不一定有用:您通常需要同步多个操作(Contains,然后Add),这样一个集合同步不同的操作并不是那么有用
  • 可以更多地使用using / lock模式 - 也许允许它们共享可重用(可扩展?)语法;您可以通过返回IDisposable并使用using来模拟这一点,但它可能更清晰
  • 迭代器块:没有提前检查参数的简单方法(而不是懒惰)。当然,你可以写两个链式方法,但那很难看
  • 更简单的不变性会很好; <#3}}帮助a bit,但还不够
  • 没有“这个ref-type参数不能为空”支持 - 尽管契约(在4.0中)对此有所帮助。但是像Foo(SqlConnection! connection)这样的语法(注入空检/ throw)会很好(与int?等对比)
  • 缺乏对泛型的运算符和非默认构造函数的支持; C#4.0使用dynamic解决了这个问题,或者您可以启用它like this

  • foreach扩展中声明之外的迭代器变量,意味着anon-methods / lambdas捕获单个变量,而不是每次迭代一次(线程/异步很痛苦) /等)

答案 1 :(得分:60)

TextWriter是StreamWriter的基础类。跆拳道?

总是让我感到困惑。

答案 2 :(得分:42)

一个小的C#pet peev - 构造函数使用C ++ / Java语法,使构造函数与类同名。

New()ctor()本来会更好。

当然,像coderush这样的工具使重命名类不再是一个问题,但从可读性POV来看,New()提供了很好的清晰度。

答案 3 :(得分:39)

我强烈赞同this post(对于那些缺乏ToString的人来说,有一个调试器属性可以为你的班级提供自定义格式。)

在上面的列表之上,我还会添加以下合理的请求:

  1. 非可空引用类型,作为可空值类型的补充,
  2. 允许覆盖struct的空构造函数,
  3. 允许泛型类型约束来指定密封类
  4. 我同意这里的另一张海报,当用作约束时请求任意构造函数签名,即。其中T:new(string),或者T:new(string,int),
  5. 我同意另一张关于修复事件的海报,包括空事件列表和并发设置(虽然后者很棘手),
  6. 运算符应该被定义为扩展方法,而不是作为类的静态方法(或者至少不是静态方法),
  7. 允许接口的静态属性和方法(Java有这个,但C#没有),
  8. 允许在对象初始值设定项中初始化事件(目前只允许字段和属性),
  9. 为什么“对象初始值设定项”语法仅在创建对象时可用?为什么不随时提供它,即。 var e = new Foo(); e {Bar = baz};
  10. 修复二次可枚举行为,
  11. 所有集合都应该具有不可变的快照以进行迭代(即,变更集合不应该使迭代器无效),
  12. 元组很容易添加,但是像“Either”这样的高效封闭代数类型不是,所以我喜欢某种方式来声明一个封闭的代数类型并对其强制执行详尽的模式匹配(基本上是对它的一流支持)访客模式,但效率更高);所以只需要获取枚举,使用详尽的模式匹配支持扩展它们,并且不允许无效的情况,
  13. 我一般都喜欢支持模式匹配,但至少支持对象类型测试;我还有点像这里的另一篇文章中提出的切换语法,
  14. 我同意另一篇文章,System.IO类,如Stream,设计有点差;任何需要一些实现抛出NotSupportException的接口都是一个糟糕的设计,
  15. IList应该比它简单得多;事实上,对于许多具体的集合接口,例如ICollection,
  16. ,情况可能如此
  17. 太多的方法抛出异常,例如IDictionary,
  18. 我更喜欢一种比Java更好的检查异常形式(参见类型和效果系统的研究,了解如何做到这一点),
  19. 在通用方法重载决策中修复各种烦人的角落情况;例如,尝试提供两个重载的扩展方法,一个对引用类型进行操作,另一个对可空的结构类型进行操作,并查看类型推断是如何的,
  20. 提供了一种安全地反映INotifyPropertyChanged等接口的字段和成员名称的方法,它将字段名称作为字符串;你可以通过使用带有MemberExpression的lambda的扩展方法来实现这一点,即。 ()=&gt; Foo,但效率不高,
  21. 允许接口中的运算符,并使所有核心数类型实现IArithmetic;其他有用的共享操作员界面也是可能的,
  22. 使变更对象字段/属性变得更加困难,或者至少允许注释不可变字段并使类型检查器强制执行它(只是将其视为仅限getter-property的属性,这并不难!);事实上,以更合理的方式统一字段和属性,因为两者都没有意义; C#3.0的自动属性是朝着这个方向迈出的第一步,但它们还远远不够,
  23. 简化声明构造函数;我喜欢F#的方法,但是这里的其他帖子只需要“新”而不是类名,至少更好,
  24. 现在我认为这已足够了。这些都是我在过去一周遇到的烦恼。如果我真的想到它,我可能会继续几个小时。 C#4.0已经添加了命名,可选和默认参数,我强烈赞同。

    现在提出一个不合理的请求:

    1. 如果C#/ CLR可以支持类型构造函数多态,那么真的,真的很好,即。仿制药的泛型,
    2. 非常好吗? : - )

答案 4 :(得分:29)

我不明白你做不到

其中T:new(U)

因此,您声明泛型类型T具有非默认构造函数。

修改

我想这样做:

public class A 
{
    public A(string text) 
    {

    }
}


public class Gen<T> where T : new(string text) 
{

}

答案 5 :(得分:20)

我真的很惊讶我是第一个提到这个的人:

ADO.NET类型化数据集不会将可空列作为可空类型的属性公开。你应该能够写下这个:

int? i = myRec.Field;
myRec.Field = null;

相反,你必须写这个,这只是愚蠢的:

int? i = (int?)myRec.IsFieldNull() ? (int?)null : myRec.Field;
myRec.SetFieldNull();

这在.NET 2.0中很烦人,而且你现在必须在你漂亮的LINQ查询中使用如上所述的jiggery-pokery,这更令人讨厌。

生成的Add<TableName>Row方法同样对可空类型的概念不敏感也很烦人。由于生成的TableAdapter方法不是,所以更是如此。

在.NET中没有太多让我觉得开发团队说“好吧,男孩们,我们足够接近 - 运送它!”但确实如此。

答案 6 :(得分:20)

  1. 我不是Stream,StringWriter,StringReader,TextReader,TextWriter类的忠实粉丝......它只是不直观的是什么。
  2. IEnumerable.Reset为迭代器抛出异常。我有一些第三方组件,在数据绑定时总是调用reset,要求我首先使用这些组件来使用这些组件。
  3. Xml Serializer应该有序列化的IDictionary元素
  4. 我完全忘记了HttpWebRequest&amp; FTP API我的痛苦....(感谢Nicholas的评论提醒我:-)
  5. 编辑
     5.我的另一个烦恼是System.Reflection.BindingFlags如何根据您使用的方法有不同的用途。在FindFields中,例如CreateInstance或SetField是什么意思?在这种情况下,他们已经超载了这个枚举背后的含义,这令人困惑。

答案 7 :(得分:15)

我不知道我会说它是一个设计缺陷,但是如果你能用VB中的方式推断一个lambda表达式会非常好:

VB:

Dim a = Function(x) x * (x - 1)

C#

如果能做到这一点会很好:

var a = x => x * (x - 1);

而不是必须这样做:

Func<int, int> a = x => x * (x - 1);

我意识到它不会长久,但在Code Golf中,每个角色都算死了!他们在设计这些编程语言时不要考虑到这一点吗? :)

答案 8 :(得分:12)

令我恼火的一件事是Predicate<T> != Func<T, bool>悖论。他们都是T -> bool类型的代表,但他们并不是分配兼容的。

答案 9 :(得分:12)

  1. System.Object班级:

    • Equals和GetHashCode - 并非所有类都具有可比性或可清除性,应移至界面。想到IEquatable或IComparable(或类似的)。

    • ToString - 并非所有类都可以转换为字符串,应该移动到接口。想到了IFormattable(或类似的)。

  2. ICollection.SyncRoot属性:

    • 促进设计不佳,外部锁几乎总是更有用。
  3. 泛型应该从一开始就存在:

答案 10 :(得分:11)

我不喜欢C#switch语句。

我想要这样的东西

switch (a) {
  1    : do_something;
  2    : do_something_else;
  3,4  : do_something_different;
  else : do_something_weird; 
}

因此不再有中断(容易忘记)以及逗号分隔不同值的可能性。

答案 11 :(得分:11)

有些人(ISV)希望您可以在构建时将其编译为机器代码并链接它,以便创建不需要dotNet运行时的本机可执行文件。

答案 12 :(得分:10)

我们非常了解正确 OO技术。解耦,合同编程,避免不正确的继承,适当使用异常,开放/封闭主体,Liskov可替代性等。但是,.Net框架还没有采用最佳实践。

对我来说,.Net设计中最大的一个缺陷就是没有站在巨人的肩膀上; 向使用其框架的大量程序员推广不太理想的编程范例

如果MS关注这一点,那么软件工程世界在这十年中可以在质量,稳定性和可扩展性方面取得巨大飞跃,但唉,它似乎正在倒退。

答案 13 :(得分:10)

C#中的事件,您必须明确检查侦听器。不是那个事件的重点,向那些碰巧在那里的人广播?即使没有?

答案 14 :(得分:9)

嵌套/递归iterators的O(N ^ 2)行为可怕(并且对大多数人来说是非常不可见的)。

我很清楚,他们知道这件事,知道how to fix it,但它并没有被视为有足够的优先权值得包容。

我一直在使用树状结构,并且当他们无意中以这种方式引入非常昂贵的操作时,必须纠正智能人员的代码。

“fore foreach”的美妙之处在于更简单,更简单的语法鼓励正确,高效的代码。这是我认为在为平台的长期成功添加新功能之前我们应该追求的"pit of success"

答案 15 :(得分:7)

接口中的静态成员和嵌套类型。

当接口成员具有特定于接口的类型的参数(例如enum)时,这尤其有用。将enum类型嵌套在接口类型中会很好。

答案 16 :(得分:7)

某些类实现接口,但它们没有实现该接口的许多方法,例如,Array实现IList,但9个方法中有4个抛出NotSupportedException http://msdn.microsoft.com/en-us/library/system.array_members.aspx

答案 17 :(得分:6)

    无处不在
  • null

  • const无处。

  • API不一致,例如变异数组会返回void,但附加到StringBuffer会返回相同的可变StringBuffer

  • 集合接口与不可变数据结构不兼容,例如Add中的System.Collections.Generic.IList<_>无法返回结果。

  • 没有结构类型,因此您只需System.Windows.Media.Effects.SamplingMode.Bilinear而不是Bilinear

  • 当类应该是不可变的IEnumerator时,由类实现的可变struct接口。

  • 平等和比较是一团糟:你有System.IComparableEquals但是你还得到System.IComparable<_>System.IEquatable,{{1 },System.Collections.IComparerSystem.Collections.IStructuralComparableSystem.Collections.IStructuralEquatableSystem.Collections.Generic.IComparer

  • 元组应该是结构,但结构不必要地禁止尾部调用消除,因此最常见和最基本的数据类型之一将不必要地分配并破坏可扩展的并行性。

答案 18 :(得分:6)

事件非常危险的默认性质。由于订阅者被删除,您可以调用事件并处于不一致状态这一事实非常可怕。有关此主题的更多阅读,请参阅Jon Skeet'sEric Lippert's优秀文章。

答案 19 :(得分:5)

0兼职作为枚举

枚举的特点:http://blogs.msdn.com/abhinaba/archive/2007/01/09/more-peculiarites-of-enum.aspx

如这个好例子所示: http://plus.kaist.ac.kr/~shoh/postgresql/Npgsql/apidocs/Npgsql.NpgsqlParameterCollection.Add_overload_3.html

我的建议,好好利用“@”标志:

而不是:

if((myVar&amp; MyEnumName.ColorRed)!= 0)

使用它:

if((myVar&amp; MyEnumName.ColorRed)!= @ 0)

答案 20 :(得分:5)

添加其他人已经提出的长点清单:

  • DateTime.Now == DateTime.Now在大多数情况下,但不是所有情况。

  • String这是不可变的,有很多构造和操作选项,但StringBuilder(可变)却没有。

  • Monitor.EnterMonitor.Exit应该是实例方法,因此您可以新建一个Monitor而不是新建一个特定的锁定对象。

  • 析构函数永远不应该被命名为析构函数。 ECMA规范称它们为终结器,这对C ++人群来说要少得多,但语言规范仍然将它们称为析构函数。

答案 21 :(得分:4)

我们使用属性的方式有时会让我感到恼火。我喜欢将它们视为Java的getFoo()和setFoo()方法的等价物。但他们不是。

如果Property Usage Guidelines声明属性应该能够以任何顺序设置,以便序列化可以工作,那么它们对于setter-time验证是无用的。如果您来自某个背景,您希望阻止某个对象进入无效状态,则属性不是您的解决方案。有时我没有看到它们比公共成员更好,因为我们在属性中应该做什么样的事情是如此有限。

为此,我总是希望(这主要是在这里大声思考,我只是希望我可以做这样的事情),我可以以某种方式扩展属性语法。想象一下这样的事情:


private string password;

public string Password
{
    // Called when being set by a deserializer or a persistence
    // framework
    deserialize
    {
       // I could put some backward-compat hacks in here. Like
       // weak passwords are grandfathered in without blowing up
       this.password = value;
    }
    get
    {
       if (Thread.CurrentPrincipal.IsInRole("Administrator"))
       {
           return this.password;
       }
       else
       {
           throw new PermissionException();
       }
    }
    set
    {
       if (MeetsPasswordRequirements(value))
       {
           throw new BlahException();
       }
       this.password = value;
    }
    serialize
    {
        return this.password;
    }
}

我不确定这是否有用或者它访问的内容是什么样的。但我只是希望我可以用属性做更多的事情,并且真正对待它们就像获取和设置方法一样。

答案 22 :(得分:4)

扩展方法很不错,但它们是一种难以解决的问题,可以用真正的混合解决问题(看看红宝石,看看我在说什么),关于mixins这个问题。将它们添加到语言中的一种非常好的方法是允许将泛型用于继承。这允许您以面向对象的方式扩展现有类:

public class MyMixin<T> : T
{
    // etc...
}

这可以像这样用来扩展字符串,例如:

var newMixin = new MyMixin<string>();

它比扩展方法强大得多,因为它允许你覆盖方法,例如包装它们允许在语言中使用类似AOP的功能。

抱歉咆哮: - )

答案 23 :(得分:3)

框架V1中的SqlCommand上的.Parameters.Add()方法设计得非常糟糕 - 如果传入一个值为(int)为0的参数,其中一个重载基本上不起作用 - 这导致他们在SqlCommand类上创建.Parameters.AddWithValue()方法。

答案 24 :(得分:3)

  1. 没有ICollection<T>IList<T>的子集;至少,协变的只读集合接口IListSource<out T>(带有枚举器,索引器和Count)将非常有用。
  2. .NET不支持weak delegates。解决方法充其量是笨拙的,并且听众方面的解决方法在部分信任中是不可能的(需要ReflectionPermission)。
  3. Generic interface unification is forbidden即使它有意义也没有问题。
  4. 与C ++不同,.NET中不允许使用covariant return types
  5. 无法按位比较两个值类型的相等性。在功能“persistent”数据结构中,我正在编写一个Transform(Sequence<T>, Func<T,T>)函数,需要快速确定函数是返回相同的值还是不同的值。如果函数不修改其大部分/全部参数,则输出序列可以共享输入序列中的部分/全部内存。如果没有按比例比较任何值类型T的能力,必须使用更慢的比较,这会极大地损害性能。
  6. .NET似乎无法以高效的方式支持ad-hoc接口(如Go或Rust中提供的接口)。这样的接口允许你将List<T>转换为假设的IListSource<U>(其中T:U),即使该类没有明确地实现该接口。至少有three different libraries(独立编写)来提供此功能(当然,如果有完美的解决方法,则存在性能缺陷,这对于称它为.NET中的一个缺陷。)
  7. 其他性能问题:IEnumerator每次迭代需要两次接口调用。普通方法指针(IntPtr大小的开放委托)或值类型委托(IntPtr * 2)是不可能的。固定大小的数组(任意类型T)不能嵌入类中。没有WeakReference<T>(您可以轻松编写自己的,但它将在内部使用强制转换。)
  8. 相同的委托类型被认为不兼容(没有隐式转换)这一事实在某些情况下对我来说是一件麻烦事(例如Predicate<T> vs Func<T,bool>)。我经常希望我们可以为接口和委托设置structural typing,以实现组件之间更松散的耦合,因为在.NET中,独立DLL中的类实现相同的接口是不够的 - 它们还必须共享一个公共引用到第三个定义接口的DLL。
  9. DBNull.Value存在,即使null同样可以达到同样的目的。
  10. C#没有?? =运算符;你必须写variable = variable ?? value。实际上,C#中有一些地方不必要地缺乏对称性。例如,你可以写if (x) y(); else z();(没有大括号),但你不能写try y(); finally z();
  11. 创建线程时,不可能导致子线程从父线程继承线程局部值。 BCL不仅不支持此功能,而且除非您手动创建所有线程,否则无法自行实现;即使有一个线程创建事件,给定线程的.NET can't tell you the "parents" or "children"
  12. 不同数据类型有两种不同的长度属性,“长度”和“计数”,这是一个小麻烦。
  13. 我可以永远继续关于the poor design of WPF ......而WCF(虽然对某些场景非常有用)也充满了瑕疵。一般来说,许多BCL新的子库的臃肿,不直观和有限的文档使我不愿意使用它们。很多新东西可以更简单,更小,更易于使用和理解,更松散耦合,更好地记录,适用于更多用例,更快速和/或更强类型。
  14. 我经常被属性getter和setter之间的不必要耦合所困扰:在派生类或派生接口中,当基类或基接口只有getter时,你不能简单地添加一个setter;如果你覆盖一个getter,那么就不允许你定义一个setter;并且您不能将setter定义为virtual,而将getter定义为非virtual。

答案 25 :(得分:3)

  • 能够调用扩展程序 null变量的方法是有争议的 e.g。

    对象a = null; a.MyExtMethod(); //这是可调用的,假定它定义了MyExtMethod

    它可能很方便但在空引用异常主题上不明确。

  • 一个命名'缺陷'。 System.configuration.dll中“配置”的“C”应大写。

  • 异常处理。应该像Java一样强行捕获或抛出异常,编译器应该在编译时检查它。用户不应该依赖于目标调用中的异常信息的注释。

答案 26 :(得分:3)

Microsoft不会修复框架中的明显错误,也不会提供挂钩,因此最终用户可以修复它们。

此外,没有办法在运行时对.NET可执行文件进行二进制修补,也无法在没有二进制修补本机库(拦截加载调用)的情况下指定.NET框架库的私有版本,而且ILDASM不可再发行无论如何,我无法自动化补丁。

答案 27 :(得分:2)

在1.x中给我带来的一件事是在使用System.Xml.XmlValidatingReader时,ValidationEventHandler的{​​{1}}没有公开基础ValidationEventArgs(标记为内部) )其中包含XmlSchemaExceptionlinenumber等所有有用信息。相反,您应该从Message字符串属性中解析它,或使用反射来挖掘它。当您想要向最终用户返回更消毒的错误时,情况就不那么好了。

答案 28 :(得分:1)

不喜欢它不能在另一个枚举中使用一个枚举的值,例如:

    enum Colors { white, blue, green, red, black, yellow }

    enum SpecialColors { Colors.blue, Colors.red, Colors.Yellow } 

答案 29 :(得分:0)

我老实说,在我多年的.NET(C#)编程期间,我没有在我记得的框架设计中存在缺陷;这意味着在我的情况下可能没有值得记住的缺陷。

然而,在微软发布XNA的几年前,我有些不喜欢的东西,他们完全削减了他们的MDX 2.0版本,这让我的游戏无法播放,也不容易转换。这是一个更广泛的缺陷,与.NET框架无关。

.NET框架实际上遵循了许多高端语言架构开发的很多非常好的设计指南。所以我不得不说我对.NET感到高兴。

但是为了告诉你一些可能更好的东西,我不得不抱怨Generic系统,我没有找到接口的Generics,例如“其中T是MyObj”(这不是完全正确的语法。但是,这部分本来可以做得更好更清晰。

想象一下,有一个2个不同类共享的接口,如果你想在该接口中使用Generic方法,你需要查看一些讨厌的Generics-sytanx。可能只是我想要做奇怪的事情。但对我来说只是一件令人难忘的事。

答案 30 :(得分:0)

StreamWriter和StreamReader类(和子类)在Close()和Dispose()上关闭底层流。无法计算我在这个设计选择中工作了多少次。

答案 31 :(得分:0)

隐式输入的变量在IMO中实施得很差。我知道你在使用Linq表达式时应该只使用它们,但是你不能在局部范围之外声明它们,这很烦人。

来自MSDN:

  • var只能在同一语句中声明和初始化局部变量时使用;该变量不能初始化为null,也不能初始化为方法组或匿名函数。
  • var不能用于类范围的字段。
  • 使用var声明的变量不能在初始化表达式中使用。换句话说,这个表达式是合法的:int i =(i = 20);但是这个表达式会产生编译时错误:var i =(i = 20);
  • 无法在同一语句中初始化多个隐式类型变量。
  • 如果名为var的类型在范围内,则var关键字将解析为该类型名称,并且不会被视为隐式类型的局部变量声明的一部分。

我认为这是一个糟糕的实现的原因是他们称之为var,但它距离变种还有很长的路要走。它实际上只是简写语法,无需输入完全类名(除了与Linq一起使用时)

答案 32 :(得分:-1)

CLR(以及C#)不支持多重继承,ASP.NET充满了LSP中断......

这些是我的“最爱”......

我可能会发现更多的错误,但那些是我最不喜欢的...... !! :(