【问题标题】:C# (.NET) Design Flaws [closed]C#(.NET)设计缺陷[关闭]
【发布时间】:2019-09-09 15:44:21
【问题描述】:

一般来说,C# 或 .NET Framework 中最大的设计缺陷是什么?

示例:不存在不可为空的字符串类型,从 IDataReader 获取值时必须检查 DBNull。

【问题讨论】:

  • 这些设计缺陷在什么意义上?
  • 使用 IDataReader 您可以使用 IsDBNull 而不是手动检查
  • 提示 Jon Skeet 谈论密封类 ;)
  • 使用扩展方法修复 IDataReader 非常容易:请参阅weblogs.asp.net/skillet/archive/2008/06/18/…
  • @lagerdalek - 如果可以的话,我会 +1 评论;好记

标签: c# .net


【解决方案1】:
  • IEnumerator<T> 上的 Reset() 方法是一个错误(对于迭代器块,语言规范甚至要求这会引发异常)
  • 在 Eric 看来,返回数组的反射方法是 a mistake
  • 数组协方差过去和现在都是一个奇怪的问题
    • 更新:带有 .NET 4.0 的 C# 4.0 为泛型接口(如 IEnumerable<out T>Func<in T, out TResult>,但不包括具体类型(如 List<T>)添加协变/逆变支持。
  • ApplicationException 相当失宠 - 这是一个错误吗?
  • 同步集合 - 一个好主意,但在现实中不一定有用:您通常需要同步 多个 操作(Contains,然后是 Add),因此同步不同操作的集合是没那么有用
    • 更新:The System.Collections.Concurrent typesTryAddGetOrAddTryRemove 等已在 .NET Framework 4.0 中添加 - 尽管接受工厂委托的方法不保证每个密钥只会调用一次工厂.
  • 可以更多地使用using/lock 模式——也许允许它们共享可重用(可扩展?)语法;您可以通过返回IDisposable 并使用using 来模拟这一点,但它本来可以更清楚
  • 迭代器块:没有简单的方法提前检查参数(而不是懒惰地)。当然,您可以编写两个链式方法,但这很难看
  • 更简单的不变性会很好; C# 4.0 帮助 a bit,但还不够
  • 不支持“此引用类型参数不能为空” - 尽管合同(在 4.0 中)对此有所帮助。但是像Foo(SqlConnection! connection)(注入空检查/throw)这样的语法会很好(与int?等形成对比)
  • 缺乏对带有泛型的运算符和非默认构造函数的支持; C# 4.0 用dynamic 解决了这个问题,或者你可以启用它like this
  • 迭代器变量在foreach 扩展中被声明外部,这意味着anon-methods/lambdas 捕获单个变量,而不是每次迭代一个(对于线程/异步/等很痛苦)

【讨论】:

  • IEnumerable != IEnumerable 确实很奇怪
  • 嗯,IEnumerable 是 1.1 的后遗症;您至少可以将 .Cast() 与 LINQ 一起使用
  • BCL 的人说ApplicationException 是一个错误——不像他们希望的那样有用。他们还说System.Exception应该是abstract
  • Non-nullable:将常规引用类型 T 传递给采用不可空 T 的东西应该是编译错误! (就像你不能将 int? 传递给 int 一样)。当然,通过另一个方向也没问题。
  • @Jon Harrop:恕我直言,应该支持不可变数组和只读数组引用。可能也适用于其他一些数组变体(例如,“可调整大小的数组”(间接引用),或具有偏移和绑定的数组引用)。
【解决方案2】:

TextWriter 是 StreamWriter 的 base 类。什么鬼?

这总是把我弄糊涂了。

【讨论】:

  • +1 我每次都得查一下。 (Whataddaya 意思是我不能 new TextWriter()?)
  • 感谢上帝...我以为只有我一个人。
  • ?它总是写文本的东西,但只有 StreamWriter 对流这样做。看起来很简单。
  • StreamWriter 这个名字并不能说明它在 IMO 上写的文本足够明显。听起来像是孤立地写字节,而 TextWriter 将是一个奇特的实现,它将 (string toWrite) 的 api 转换为您的字节。现在如果它被称为 StreamTextWriter 那么肯定,它会立即显而易见但有点长:(
【解决方案3】:

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

New()ctor() 会更好。

当然,诸如 coderush 之类的工具使重命名类的问题不再那么严重,但从可读性 POV 来看,New() 提供了极大的清晰度。

【讨论】:

  • Erm,它怎么知道你要创建一个新实例?
  • @BlueRaja:Scott 指的是类中构造函数的命名。 class Foo { new(int j) {i = j} int i; }
  • 虽然我 100% 同意 ctor() 或 constructor() 会更好(不是 New--大写关键字违反约定),但我不愿称其为设计缺陷。他们想吸引现有的 C++/Java 开发人员,借用许多愚蠢的旧语法约定可以说帮助他们实现了目标。
  • 与这个问题相关:stackoverflow.com/questions/32101993/c-sharp-sorted-linkedlist 没有办法(仅使用 .NET)简单地使用 MergeSort、bucketsort 或任何其他排序算法对 LinkedList 进行排序,而是使用比 ad hoc 慢得多的 linq实施。
【解决方案4】:

我非常同意this post(对于那些对缺少 ToString 感到厌烦的人,有一个调试器属性可以为您的类提供自定义格式)。

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

  1. 不可为空的引用类型作为可空值类型的补充,
  2. 允许覆盖结构的空构造函数,
  3. 允许泛型类型约束指定密封类,
  4. 我同意这里的另一张海报,它在用作约束时要求任意构造函数签名,即。 T : new(string),或T : new(string, int)
  5. 我也同意此处关于修复事件的另一张海报,无论是针对空事件列表还是在并发设置中(尽管后者很棘手),
  6. 操作符应该定义为扩展方法,而不是类的静态方法(或至少不只是静态方法),
  7. 允许接口的静态属性和方法(Java 有,但 C# 没有),
  8. 允许在对象初始化器中初始化事件(目前只允许字段和属性),
  9. 为什么“对象初始化器”语法只能在创建对象时使用?为什么不随时提供它,即。 var e = new Foo(); e { Bar = baz };
  10. fix quadratic enumerable behaviour,
  11. 所有集合都应具有用于迭代的不可变快照(即,改变集合不应使迭代器失效),
  12. 元组很容易添加,但像“Either<T>”这样的有效封闭代数类型却不是,所以我喜欢某种方式来声明一个封闭代数类型并对其强制执行详尽的模式匹配(基本上是一流的支持对于访问者模式,但效率更高);所以只需要枚举,用详尽的模式匹配支持扩展它们,并且不允许无效的情况,
  13. 我希望支持一般的模式匹配,但至少支持对象类型测试;我也有点喜欢这里另一篇文章中提出的 switch 语法,
  14. 我同意另一篇文章,即System.IO 类,如Stream,设计有些糟糕;任何需要一些实现来抛出NotSupportedException 的接口都是糟糕的设计,
  15. IList 应该比现在简单得多;事实上,对于许多具体的集合接口来说,这可能是正确的,比如ICollection
  16. 抛出异常的方法太多,例如 IDictionary,
  17. 我更喜欢检查异常的形式,而不是 Java 中可用的形式(有关如何做到这一点,请参阅关于类型和效果系统的研究),
  18. 修复了泛型方法重载决议中各种恼人的极端情况;例如,尝试提供两种重载扩展方法,一种对引用类型进行操作,另一种对可空结构类型进行操作,看看您的类型推断是否喜欢这样,
  19. 提供一种方法来安全地反映INotifyPropertyChanged 等接口的字段和成员名称,这些接口将字段名称作为字符串;您可以通过使用带有MemberExpression 的lambda 的扩展方法来做到这一点,即。 () => Foo,但这不是很有效,
    • 更新:C# 6.0 为单个成员名称添加了nameof() 运算符,但它在泛型中不起作用(nameof(T) == "T" 而不是实际类型参数的名称:您仍然需要这样做typeof(T).Name)) -它也不允许您获取“路径”字符串,例如nameof(this.ComplexProperty.Value) == "Value" 限制其可能的应用。
  20. 允许接口中的操作符,并使所有核号类型实现IArithmetic;其他有用的共享操作界面也是可能的,
  21. 使对象字段/属性的变异变得更加困难,或者至少允许注释不可变字段并让类型检查器强制执行它(只需将其视为仅限 getter 的属性 fer chrissakes,这并不难!);事实上,以更明智的方式统一字段和属性,因为两者兼而有之是没有意义的; C# 3.0 的自动属性是朝这个方向迈出的第一步,但还远远不够,
    • 更新:虽然 C# 有 readonly 关键字,并且 C# 6.0 添加了只读自动属性,但它不像真正的语言对不可变类型和值的支持那么严格。
  22. 简化声明构造函数;我喜欢 F# 的方法,但这里的其他帖子至少需要“new”而不是类名更好,

我想这已经足够了。这些都是我在过去一周遇到的所有烦恼。如果我真的全神贯注,我可能会持续几个小时。 C# 4.0 已经添加了命名、可选和默认参数,我非常赞成。

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

  1. 如果 C#/CLR 可以支持类型构造函数多态性,那将是真的,真的很好,即。泛型优于泛型,

漂亮吗? :-)

【讨论】:

  • 关于#1,每个类型必须要么有一些默认值,要么系统必须提供一种在分配特定类型的变量或字段时运行构造函数的方法。我更喜欢后者(#2的一个分支),但如果可以修饰非虚拟方法/属性以指定应该在没有空检查的情况下调用它们,则可以容纳#1。这将允许像“String”字段这样的东西表现得好像它们默认为空字符串而不是 null(因为如果在 null 字符串上调用 String 的静态“length”函数可能返回 0)。
  • 关于#2,如果结构体不仅可以指定除了零填充之外的构造函数,而且还可以指定除字节对字节复制之外的复制构造函数,那么它通常会很有用.实际上,我真的很想看到一个类似 .net 的框架,它可以很好地识别实体可能具有值或引用语义,并允许将值类型的堆对象标记为可变、共享不可变或未提交(如果未提交的值对象的模式首先是 CompareExchange'd 到 mutable,则它可能会发生变异,或者如果它的模式首先是 CompareExchange'd 到 shared,则可能会被共享。
  • 优秀的积分!对于#1,类型系统注释是常见的解决方案,但我更喜欢通过类型变量传播构造函数约束,例如 T:new()。回复:#2,关于复制构造函数的观点很好,但我会很高兴按照我上面描述的方式使用更通用的构造函数。更好的是完全消除构造函数作为可区分方法,并简单地使它们成为静态方法。这允许更简单和更通用的构造模式,特别是如果我们允许接口上的静态方法。 Constructors-as-static-methods + static-methods-in-interfaces 也解决了 #1。
  • #3:使用密封类作为泛型类型参数有什么意义,例如Foo 在哪里 T:string? #11:好的,所以我有一个 List<T> 的一百万 Ts。您如何建议有效地拍摄快照? #21:使用readonly 关键字......虽然这里有一些很好的建议,但它们大多只是建议,而不是设计缺陷。
  • 这是一个非常有趣的答案,但我认为我们应该使用 C# 6 功能进行更新。示例:第 19 项和第 21 项已实施 =)
【解决方案5】:

我不明白你不能这样做

其中 T : 新(U)

所以你声明泛型类型 T 有一个非默认构造函数。

编辑:

我想这样做:

public class A 
{
    public A(string text) 
    {

    }
}


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

}

【讨论】:

  • 泛型类型声明了您将如何使用该类型的对象,即它们的接口。构造函数是该接口的一个实现细节,这不是消费者关心的。当您需要创建参数化实例时,请使用工厂。
  • 对于信息,虽然它不能进行编译时检查,但 MiscUtil 中有一些代码可以有效地使用非默认构造函数(在泛型中),即没有 Activator.CreateInstance 或反射。跨度>
  • 因为它没有意义并且在某些用途上令人困惑。尽管如此,在处理不可变对象时它还是很有用的。
  • 一般来说,缺少成员约束很烦人,是的。
  • 阿门,我一直想要这个
【解决方案6】:

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

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&lt;TableName&gt;Row 方法对可空类型的概念同样不敏感。更是如此,因为生成的 TableAdapter 方法不是。

.NET 中并没有太多让我觉得开发团队说“好的,孩子们,我们已经足够接近了 - 发布它!”但这肯定会。

【讨论】:

  • 我完全同意!每次我必须使用它时(经常如此),这都会让我感到困扰。啊! +1
  • 他们能做的至少是想出一个新的 DataSetV2 类(坏名字 - 只是为了论证),它始终使用可空值类型而不是 DBNull。
  • 我们不要忘记需要一个特殊值DBNull.Value 的荒谬之处,而null 本身完全足以表示NULL。幸运的是 LINQ-to-SQL 只是将 null 用于 NULL。
  • 实际上,荒谬是整个荒谬大厦的基石。
【解决方案7】:
  1. 我不是 Stream、StringWriter、StringReader、TextReader、TextWriter 类的忠实粉丝...只是不直观。
  2. IEnumerable.Reset 为迭代器抛出异常。我有一些第三方组件在数据绑定时总是调用重置,需要我先转换到列表才能使用这些组件。
  3. Xml 序列化程序应该有序列化的 IDictionary 元素
  4. 我完全忘记了 HttpWebRequest 和 FTP API,这让我很痛苦....(感谢 Nicholas 的评论提醒我这一点:-)

编辑
5. 我的另一个烦恼是 System.Reflection.BindingFlags 如何根据您使用的方法有不同的用途。例如在 FindFields 中 CreateInstance 或 SetField 是什么意思?在这种情况下,他们重载了这个枚举背后的含义,这令人困惑。

【讨论】:

  • +1 我每次都必须查找任何 XmlTextWriter、TextWriter 等类。与 HttpWebRequest/Response 相同。那里完全不直观的 API。
  • +1-1=0: XmlTextWriter 等,我同意不可能从名称中真正推断出它们是什么。 HttpWebRequest 我不同意我觉得它很直观。
  • 我想每个人都是他自己的。我想对于 FTP,我期望比他们拥有的更高级别的抽象。
【解决方案8】:

我不知道我会说这是一个设计缺陷,但如果你能像在 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 中,每个角色都该死!他们在设计这些编程语言时没有考虑到这一点吗? :)

【讨论】:

  • 微软在设计语言时应该考虑 Code Golf 吗?
  • @Ray Burns:它在 VB 中是如何知道的? VB支持,那有什么区别呢?
  • @RayBurns 类型推断?自 1989 年以来我一直在使用它。
  • Lamdas 在 C# 中是同音字。片段(int x) =&gt; x * (x -1); 可以表示Func&lt;int, int&gt;,也可以表示Expression&lt;Func&lt;int, int&gt;&gt;
  • @BenAlabaster:VB 支持后期绑定算术运算符。 C# 必须在编译时解决它们。这是语言差异。例如 VB 可以将两个对象加在一起。 C# 不能,因为 + 没有为 object 定义。
【解决方案9】:
  1. System.Object 类:

    • Equals 和 GetHashCode - 并非所有类都具有可比性或可散列性,应将其移至接口。会想到 IEquatable 或 IComparable(或类似)。

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

  2. ICollection.SyncRoot 属性:

    • 宣传糟糕的设计,外部锁几乎总是更有用。
  3. 泛型应该从一开始就存在:

【讨论】:

  • 1.这些方法是如此普遍,以至于认为奇怪的情况是可以的,是否有人可以在您的班级上调用 Object.Equals 是否重要?众所周知,可能有也可能没有实现,并且在 99% 的类上要求 IEquatable、IFormattable 是奇怪的。
  • 这很有用,例如使用默认的引用相等构造一个以用户定义的对象为键的字典,而无需为此目的显式地将代码添加到用户定义的对象。我认为 Finalize 是一个更大的浪费(更好的选择是让需要终结的对象实现 iFinalizable 并显式注册自己以进行终结)。 OTOH,应该有更多对 iDisposable 的固有支持,包括在构造函数抛出异常时调用 Dispose。
  • @supercat:只需正确更新EqualityComparer&lt;T&gt;.Default。然后var dict = new Dictionary&lt;object, string&gt;(EqualityComparer&lt;object&gt;.Default)var dict = new Dictionary&lt;object, string&gt;() 都将使用参考比较/相等。
  • @supercat:您所描述的正是EqualityComparer&lt;T&gt;.Default 所做的。无需在每次查找时检查。比较器是Dictionary 实例的一个属性,每个Dictionary 都知道它正在使用哪个。
  • @supercat:字典必须使用最通用的类​​型(即公共基类)作为键,在同一个字典中同时使用字符串和日期时间不会有任何意义,除非使用引用比较,除非提供了用户定义的比较。请记住,本主题的名称是“C# (.NET) Design Flaws”。
【解决方案10】:

让我恼火的是Predicate&lt;T&gt; != Func&lt;T, bool&gt; 悖论。它们都是 T -&gt; bool 类型的代表,但它们不兼容分配。

【讨论】:

  • 有一个技巧使用 Delegate.Create 和一些强制转换来进行转换,但至少能够进行显式强制转换会很好(但是我可以理解缺乏对隐式的支持)
  • 代表的设计总体上是有缺陷的;例如缺乏弱事件(在没有订阅者特别努力的情况下实现源端弱事件只能通过一堆反射和ReflectionPermission来完成,请参阅codeproject.com/Articles/29922/Weak-Events-in-C),以及来自委托必须是引用的要求的低效率类型(委托会更快,并且在许多情况下会使用 1/3 的内存,如果它们是值类型 - 那么它们将只是一对可以传递到堆栈上的指针。)跨度>
【解决方案11】:

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

【讨论】:

  • 您应该能够在运行之前对您的程序进行 NGEN。
  • 与移除运行时的依赖不同。您只需保存对代码的第一次 JIT 运行。
  • 难道没有一个混淆工具可以做到这一点吗?它将框架嵌入到您的 exe 中,因此您不必部署它。我不记得它的名字了……它不是 PreEmptive 的……
  • Xenocode 的 Postbuild 不这样做吗?如果 Visual Studio 有办法做到这一点,那就太好了……
【解决方案12】:

我们非常了解正确 OO 技术。解耦、契约式编程、避免不当继承、适当使用异常、开/关主体、Liskov 可替换性等。到目前为止,.Net 框架还没有采用最佳实践。

对我而言,.Net 设计的最大缺陷不是站在巨人的肩膀上;而是站在巨人的肩膀上。 向使用其框架的广大程序员推广不太理想的编程范式

如果 MS 重视这一点,软件工程界在这十年中本可以在质量、稳定性和可扩展性方面取得巨大飞跃,但可惜,它似乎正在倒退。

【讨论】:

  • +1 表示目标咆哮;我还要补充一点,无法对每个类进行子类化,缺少基本类的接口,并且即使在几年后也不愿意修复框架错误
  • 如果我们切入正题,我们是不是说 .Net 框架太糟糕以至于很难确定哪个缺陷是最糟糕的?发泄后我确实感觉好多了,并感谢大家的支持,因为我期待着被 MS 粉丝们大声喊叫。
  • 无论哪种方式我都没有投过票,而且无论如何我都非常不愿意投反对票,但我想弄清楚为什么我只是不关心。
  • 我认为您是在说“它不如它可能的那么好”,这是一个非答案。没有什么是完美的。提供细节。
  • 不,我是说设计存在明显缺陷的情况有很多,那么当您认为他们做对时,他们仍然会做错。例如,我在此处的 MSDN 论坛发帖:social.msdn.microsoft.com/forums/en-US/wpf/thread/…
【解决方案13】:

我不喜欢 C# switch 语句。

我想要这样的东西

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

所以没有更多的中断(容易忘记)并且可以用逗号分隔不同的值。

【讨论】:

  • 实际上,我认为如果它需要单个语句或大括号中的块会更好 - 就像 C# 中的其他所有内容一样。如果无法通过,当前的 break 语法有点狡猾,而且它也不限于范围。 (AFAIK,可能,我不知道)
  • 我不明白你的意思。在此处发布您自己的“理想”切换语句。我不想通过逗号分隔值。
  • 顺便说一句,我同意 OP。 switch 在所有模仿 C 故意残废版本的语言(针对速度进行了优化!)中都被彻底破坏了。 VB 的表现要好得多,但仍然落后于具有模式匹配的语言(Haskell、F# ...)。
  • tuinostel:类似于 switch (a) { case 1 { do_something; } 案例 2 { do_something_else; } } - 也就是说,去掉 break 语句需要为每种情况提供适当的代码块
  • 一般来说,中断似乎是一个错误,发出它不是编译错误吗?它似乎只是为了缓解从 C 到 C# 的过渡(也就是教导开发人员他们不能自动失败)
【解决方案14】:

C# 中的事件,您必须显式检查侦听器。这难道不是事件的重点,向碰巧在那里的任何人广播吗?就算没有?

【讨论】:

  • 当你想要它时没有糖可用于这很烦人,我明白他们为什么这样做很难,它鼓励在不需要时不实例化事件的参数
  • 嗯。我要么不理解,要么不同意,或者两者兼而有之:-)。我不能说我曾经对实例化任何东西感到气馁。这对我来说听起来像是过早的优化。
  • 在某些情况下,部分方法是一种可行的替代方案。
  • 加上这导致的所有耦合... MS 有 CAB 可以解决这两个问题,但是由于 C# 的限制,CAB 本身有很多问题(例如,字符串而不是枚举作为事件-主题)- 为什么不让松散耦合的事件成为语言的一部分!?
【解决方案15】:

嵌套/递归iterators 的糟糕(并且对大多数人来说完全不可见)O(N^2) 行为。

我很失望他们知道这件事,知道how to fix it,但它没有被视为具有足够的优先级来值得纳入。

我一直在使用类似树的结构,当聪明人无意中以这种方式引入高成本操作时,我不得不纠正他们的代码。

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

【讨论】:

  • 这是特别有缺陷的,因为它违背了对 O 行为的直觉预期,因此会困住很多人
【解决方案16】:

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

【讨论】:

  • 嗯,你不能改变数组中元素的数量,所以 Add、Clear、Insert 和 Remove(At) 只能抛出 NotSupported...任何为 IsFixedSize 返回 true 的 IList 实现都会抛出它们。
  • @C.B.我猜派对有点晚了:) 但是如果 Array 不能满足“IList”,为什么还要实现它呢?这违反了 SOLID 原则中的 L。
【解决方案17】:

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

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

【讨论】:

  • 这与您的其他建议非常相似吗?
  • 不,这个是关于C#语言的,另一个是关于框架的。不是每个人都关心区别,所以我说一个是关于允许什么,另一个是关于提供什么。
【解决方案18】:

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

【讨论】:

  • 我不介意默认情况下事件不是线程安全的(它可以提高单线程代码的性能);愚蠢的是默认情况下添加/删除是安全的,但触发事件的自然方式是不安全的,并且没有任何设施可以轻松使其安全。
  • @Qwertie:更愚蠢的是,在很长一段时间内,添加/删除会使用锁定,但仍然不是线程安全的。
【解决方案19】:
  • null 无处不在。

  • const 无处。

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

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

  • 没有结构类型,所以你写System.Windows.Media.Effects.SamplingMode.Bilinear而不是Bilinear

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

  • 1234563 、System.Collections.Generic.IComparerSystem.Collections.Generic.IEqualityComparer
  • 元组应该是结构,但结构不必要地抑制尾调用消除,因此最常见和基本的数据类型之一将不必要地分配并破坏可扩展的并行性。

【讨论】:

  • CLR 中没有结构类型,但您似乎将结构类型与类型推断或称为“符号”的 Ruby 特性混合在一起。如果 CLR 认为 Func 和 Predicate 是相同的类型,或者至少是隐式可转换的,则结构类型将是。
  • 说到比较,别忘了Comparer
  • @Qwertie 我指的是 OCaml 中的多态变体等编程语言特性。 OCaml 的 LablGL 库有许多有趣的结构类型示例,这些示例在图形上下文中很有用。与类型推断无关,仅与符号相关。
  • 如何使用不可变结构IEnumerator
【解决方案20】:

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

我的建议,好好利用“@”符号:

代替:

如果 ((myVar & MyEnumName.ColorRed) != 0)

使用这个:

如果 ((myVar & MyEnumName.ColorRed) != @0)

【讨论】:

  • +1 枚举是 Java 做对的少数几件事之一,而 C# 没有做对
【解决方案21】:

要添加到其他人已经提出的长长的优点列表中:

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

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

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

  • 析构函数不应该被命名为析构函数。 ECMA 规范称它们为终结器,这对 C++ 人群来说不那么容易混淆,但语言规范仍然将它们称为析构函数。

【讨论】:

  • DateTime.Now 是世界上最明显的竞争条件,但其他条件 +1
  • 与其说它是一种竞争条件,不如说是他们把它变成了一个属性。属性看起来与字段完全一样,因此 IMO 的行为相当令人惊讶。
  • @Brian Rasmussen:DateTime.Now 是一个非常恰当的属性,因为它不是通过阅读而是通过外部因素改变的。如果读取 SomeForm.Width 之类的属性,然后——在用户调整表单大小后——再次读取它,第二次读取的值将不同。虽然第一个 DateTime.Now 可能需要足够长的时间来执行它会影响第二个读取的值,但这种效果与执行花费相同时间的任何其他函数没有什么不同。
【解决方案22】:

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

如果Property Usage Guidelines 声明属性应该能够以任何顺序设置以便序列化可以工作,那么它们对于设置器时间验证毫无用处。如果您的背景是您希望防止对象允许自己进入无效状态,那么属性不是您的解决方案。有时我看不到他们比公共成员更好,因为我们在属性中应该做的事情非常有限。

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

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

我不确定这是否有用或访问这些会是什么样子。但我只是希望我可以对属性做更多的事情,并真正将它们视为 get 和 set 方法。

【讨论】:

  • 我相信他们提供了 ISerializable 接口和隐式构造函数来做这样的事情,也就是当你不希望序列化程序只调用属性时。虽然还有一些工作要做,但您似乎已经用您的方法完成了大部分工作。
【解决方案23】:

扩展方法很好,但它们是解决问题的一种丑陋的方法,这些问题本来可以用真正的 mixins 更干净地解决(看看 ruby​​ 看看我在说什么),关于 mixins 的主题。将它们添加到语言中的一个非常好的方法是允许使用泛型进行继承。这允许您以一种很好的面向对象的方式扩展现有类:

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

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

var newMixin = new MyMixin<string>();

它比扩展方法强大得多,因为它允许您覆盖方法,例如将它们包装起来,从而允许在语言中实现类似 AOP 的功能。

对不起,我的咆哮:-)

【讨论】:

  • 有趣,但我更喜欢扩展方法。如果我得到一个包含一堆字符串扩展方法的库,我不想为了获得新的东西而必须更改对 MyMixin 的所有字符串引用。当然,这有点微不足道,但是透明地添加方法是使扩展方法如此出色的原因。
  • 顺便说一句,你知道这已经有效了吗?
  • 我不明白 LINQ 是如何以这种方式工作的
  • @RCIX:Mixins 听起来像是我认为扩展方法应该工作的方式。隐式扩展方法的问题在于,它意味着真正的类成员需要优先于扩展方法。如果定义了扩展方法 Graphics.DrawParallelogram(Pen p, Point v1, Point v2, Point v3) 并且稍后将 DrawParallelogram 函数添加到使用不同顺序的点的 System.Graphics 中,则使用扩展方法的代码将中断警告。顺便说一句,使用两个点作为扩展方法会有什么问题吗(例如 object..method()?)
【解决方案24】:

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

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

【讨论】:

  • 你的意思是什么明显的框架错误?
  • #1 单击可滚动控件的部分可见子控件。控件在接收到 MouseDown 事件之前被移到视图中,从而导致单击在控件上的其他位置而不是预期的位置。在树视图上情况更糟,它也会触发拖动操作。
  • #2 this #3 this #4 this
【解决方案25】:
  • 能够调用扩展 空变量上的方法是有争议的 例如

    对象 a=null; a.MyExtMethod(); // 这是可调用的,假设它在某个地方定义了 MyExtMethod

    它可能很方便,但在空引用异常主题上是模棱两可的。

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

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

【讨论】:

  • 非常方便,不过 - 我有一个用于参数检查的“ThrowIfNull` 扩展方法;-p
  • 你能做到吗?呃 ThrowIfNull 这是一个有趣的扩展,但这似乎是错误的。
  • 在纯 CLR 中,您可以在空引用上调用实例方法,如果该方法不访问对象或其字段,则调用不会引发空引用异常。 (你不能在 C# 中这样做,因为它甚至对非虚拟方法也使用 callvirt)
  • 异常是完全错误的。如果您必须捕获可能在调用堆栈中抛出的每个该死的异常,您就不能快速失败。但我确实希望更容易识别特定调用中可能引发的任何和所有异常及其产生的调用堆栈......
  • @Will:异常处理在 Java 和 .net 中都很棘手,因为使用的机制将三个概念紧密联系在一起,这三个概念有些相关但又有些正交:(1)什么类型的东西出错了(数组边界错误、I/O 超时等); (2) 某些代码是否应该因此而采取行动; (3) 在什么时候问题应该被认为是“解决了”。考虑一个例程,该例程应该使用从 IEnumerable 读取的数据来改变对象。如果在那个 IEnumerable 的处理过程中出现异常应该怎么办?
【解决方案26】:

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

【讨论】:

  • 我同意,但我认为您的意思是 SqlCommand.Parameters.Add() 方法。
【解决方案27】:
  1. 没有ICollection&lt;T&gt;IList&lt;T&gt; 的子集;至少,协变只读集合接口 IListSource&lt;out T&gt;(带有枚举器、索引器和计数)会非常有用。
  2. .NET 不支持weak delegates。这些变通办法充其量是笨拙的,并且在部分信任的情况下,侦听器端的变通办法是不可能的(需要ReflectionPermission)。
  3. Generic interface unification is forbidden 即使它有意义且不会造成任何问题。
  4. 与 C++ 不同,.NET 中不允许使用 covariant return types
  5. 无法按位比较两个值类型是否相等。在一个功能性的“persistent”数据结构中,我正在编写一个Transform(Sequence&lt;T&gt;, Func&lt;T,T&gt;) 函数,该函数需要快速确定该函数是返回相同的值还是不同的值。如果函数不修改其大部分/所有参数,则输出序列可以共享来自输入序列的部分/全部内存。如果无法按位比较任何值类型 T,则必须使用慢得多的比较,这会极大地损害性能。
  6. .NET 似乎无法以高性能方式支持 ad-hoc 接口(如 Go 或 Rust 中提供的接口)。即使该类没有显式实现该接口,此类接口也允许您将List&lt;T&gt; 转换为假设的IListSource&lt;U&gt;(其中 T:U)。至少有 three different libraries (独立编写)来提供此功能(当然存在性能缺陷——如果可以找到完美的解决方法,将其称为 .网)。
  7. 其他性能问题:IEnumerator 每次迭代需要两个接口调用。普通方法指针(IntPtr 大小的开放委托)或值类型委托(IntPtr*2)是不可能的。固定大小的数组(任意类型的 T)不能嵌入到类中。没有WeakReference&lt;T&gt;(您可以轻松编写自己的,但它会在内部使用强制转换。)
  8. 在某些情况下(例如Predicate&lt;T&gt;Func&lt;T,bool&gt;),相同的委托类型被认为是不兼容的(没有隐式转换)这一事实对我来说是一件麻烦事。我经常希望我们可以将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 定义为虚拟,而将 getter 定义为非虚拟。

【讨论】:

  • 我同意你关于IList&lt;T&gt; 的子集,尽管我会使用IReadableByIndex&lt;out T&gt;IAppendable&lt;in T&gt;。你的许多其他事情我也同意。
  • 这个名字真长。也许我们可以在IListReader&lt;T&gt; 上妥协 ;) - 我使用“源”这个词作为“接收器”的反义词(只写接口)。
  • 可能是IListSource&lt;in T&gt;IReadableList&lt;out T&gt;。让基本接口类型包含并非在所有派生中都存在的方法是有价值的,尽管我认为让接口有些专门化通常是件好事。例如,可以有一个IList&lt;T&gt;,其中包含可能起作用或可能不起作用的调整大小的方法,以及一个实现相同方法但保证它们应该起作用的IResizableList&lt;T&gt;。这种方法在字段可能包含对可变列表的唯一现有引用或对不可变列表的共享引用的情况下可能很有用。
  • 在这种情况下,想要更改列表内容的代码将检查它是否是可变类型,如果不是,则生成一个新的可变实例,其中包含与不可变列表相同的项目,然后开始使用它。如果代码每次想要对其使用变异方法时都必须不断地对字段进行类型转换,那将是令人讨厌的。
  • @supercat 这只是令人讨厌,因为 C# 没有提供一种非常简单的方法来检查接口是否已实现并立即使用它。 MS 应该添加一个语言功能以使其更容易。我首选的技术是绑定表达式if (rl:(list as IResizableList&lt;T&gt;) != null) rl.Add(...);,但还有其他建议。作为各种集合和集合适配器的作者,让我厌烦的是编写了许多抛出异常的伪方法。作为一个类型安全爱好者,我不想被允许调用非法方法。一个 IntelliSense 粉丝,我不想看到他们列出来。
【解决方案28】:

在 1.x 中让我印象深刻的一件事是在使用 System.Xml.XmlValidatingReader 时,ValidationEventHandlerValidationEventArgs 不会暴露底层的 XmlSchemaException(标记为内部),其中包含所有有用的信息,例如linenumberposition。相反,您应该从 Message 字符串属性中解析它或使用反射来挖掘它。当您想向最终用户返回更经过清理的错误时,效果就不太好了。

【讨论】:

    【解决方案29】:

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

        enum Colors { white, blue, green, red, black, yellow }
    
        enum SpecialColors { Colors.blue, Colors.red, Colors.Yellow } 
    

    【讨论】:

    • 不过,这甚至没有意义。 typeof(Color) != typeof(SpecialColors).
    • 这很容易做到:enum SpecialColors { blue = Colors.blue, red = Colors.red, yellow = Colors.Yellow }
    【解决方案30】:

    IMO 的隐式类型变量实施得很差。我知道你真的应该只在使用 Linq 表达式时使用它们,但是你不能在本地范围之外声明它们很烦人。

    来自 MSDN:

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

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

    【讨论】:

    • 绝对是匿名类型(新 {...}),而不是隐式类型(var)
    • 只是重新阅读我发布的内容,这是错误的。我的意思是隐式类型变量虽然
    • Eric Lippert 解释了为什么 var 不能在方法之外使用,这基本上是因为它创建了一个充满可能性的黑匣子。 blogs.msdn.com/ericlippert/archive/2009/01/26/…
    • var 不是变体!它正是用于速记(尤其是匿名类型)。当它出现时享受动态......
    猜你喜欢
    • 2010-09-12
    • 2016-01-09
    • 2011-05-06
    • 2011-07-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多