在C#/ .NET中容易犯的7个危险错误

在课程“ C#ASP.NET Core Developer”开始之前,准备了本文的翻译




C#是一种很棒的语言,.NET Framework也非常好。与其他语言相比,C#中的强类型键入有助于减少可引发的错误数量。另外,与诸如JavaScript(其中true为false)之类的东西相比,其一般直观的设计也有很大帮助但是,每种语言都有其自己的优势,很容易被踩踏,还有关于该语言和基础结构预期行为的错误观念。我将尝试详细描述其中一些错误。

1.不理解延迟(懒惰)执行


我相信,经验丰富的开发人员会意识到这种.NET机制,但是这可能会使知识渊博的同事感到惊讶。简而言之,返回IEnumerable<T>并用于yield返回每个结果的方法/运算符不会在实际调用它们的代码行中执行-当以某种方式访问​​结果集合时将执行它们。注意,大多数LINQ表达式最终都将返回带有yield的结果

例如,请考虑以下令人震惊的单元测试。

[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void Ensure_Null_Exception_Is_Thrown()
{
   var result = RepeatString5Times(null);
}
[TestMethod]
[ExpectedException(typeof(InvalidOperationException))]
public void Ensure_Invalid_Operation_Exception_Is_Thrown()
{
   var result = RepeatString5Times("test");
   var firstItem = result.First();
}
private IEnumerable<string> RepeatString5Times(string toRepeat)
{
   if (toRepeat == null)
       throw new ArgumentNullException(nameof(toRepeat));
   for (int i = 0; i < 5; i++)
   {   
       if (i == 3)
            throw new InvalidOperationException("3 is a horrible number");
       yield return $"{toRepeat} - {i}";
   }
}

这些测试都将失败。第一次测试将失败,因为结果未在任何地方使用,因此该方法的主体将永远不会执行。第二个测试将由于另一个更为平凡的原因而失败。现在,我们得到调用方法的第一个结果,以确保该方法实际运行。但是,延迟执行机制将尽快退出该方法-在这种情况下,我们仅使用第一个元素,因此,一旦我们经过第一次迭代,该方法就会停止执行(因此i == 3永远不会成立)。

延迟执行实际上是一种有趣的机制,特别是因为它使链接LINQ查询变得很容易,仅当您准备好使用查询时才检索数据。

2. , Dictionary ,


这特别令人不快,而且我敢肯定,我的某个地方的代码都依赖于此假设。当您将项目添加到列表中时List<T>,它们的存储顺序与添加顺序相同-逻辑上。有时,您需要使另一个对象与列表中的项目相关联,显而易见的解决方案是使用字典Dictionary<TKey,TValue>该字典允许您为键指定相关值。

然后,您可以使用foreach遍历字典,并且在大多数情况下,字典将按预期方式工作-您将以将元素添加到字典中的顺序访问元素。但是,此行为是不确定的 -即这是一个快乐的巧合,不是您可以依靠并始终期望的事情。这在Microsoft文档,但我认为很少有人认真研究过此页面。

为了说明这一点,在下面的示例中,输出将如下所示:

第三


var dict = new Dictionary<string, object>();       
dict.Add("first", new object());
dict.Add("second", new object());
dict.Remove("first");
dict.Add("third", new object());
foreach (var entry in dict)
{
    Console.WriteLine(entry.Key);
}

不相信我?自己在线检查这里

3.不考虑流量安全


多线程很棒,如果正确实现,则可以显着提高应用程序的性能。但是,一旦您进入多线程,就应该非常小心地对待要修改的任何对象,因为如果您不够谨慎的话,您可能会遇到看似随机的错误。

简而言之,.NET库中的许多基类都不是线程安全的。-这意味着Microsoft不保证可以使用多个线程并行使用此类。如果您可以立即发现与此相关的任何问题,那么这将不是一个大问题,但是多线程的本质意味着出现的任何问题都是非常不稳定且不可预测的-最有可能的是,两个执行都不会产生相同的结果。

例如,考虑使用简单但不是线程安全的代码块List<T>

var items = new List<int>();
var tasks = new List<Task>();
for (int i = 0; i < 5; i++)
{
   tasks.Add(Task.Run(() => {
       for (int k = 0; k < 10000; k++)
       {
           items.Add(i);
       }
   }));
}
Task.WaitAll(tasks.ToArray());
Console.WriteLine(items.Count);

因此,我们将0到4的数字分别添加到列表10,000次,这意味着列表最终应包含50,000个元素。我是不是该?好吧,最终会有一个小机会-但以下是我5次不同发射的结果:

28191
23536
44346
40007
40476

您可以在此处在线检查

实际上,这是因为Add方法不是原子的,这意味着线程可以中断该方法,从而最终可以在另一个线程正在添加或添加具有相同索引的元素时调整数组的大小。作为另一个线程。 IndexOutOfRange异常出现了两次,可能是因为数组的大小在添加数组时发生了变化。那么我们在这里做什么?我们可以使用lock关键字来确保只有一个线程可以一次将(添加)项添加到列表中,但这会显着影响性能。微软是个好人,提供了一些很棒的收藏它们是线程安全的,并且在性能方面进行了高度优化。我已经发表了一篇文章,描述了如何使用它们

4.滥用LINQ中的延迟(延迟)加载


延迟加载是LINQ to SQL和LINQ to Entities(实体框架)的一项重要功能,可让您根据需要加载相关的表行。在我的另一个项目中,我有一个“模块”表和一个“结果”,它们之间具有一对多关系(一个模块可以有许多结果)。



当我想获取特定的模块时,我当然不希望实体框架返回Modules表具有的每个结果!因此,他足够聪明,可以仅在需要时执行查询以获取结果。因此,下面的代码将执行2个查询-一个用于获取模块,另一个用于获取结果(针对每个模块),

using (var db = new DBEntities())
{
   var modules = db.Modules;
   foreach (var module in modules)
   {
       var moduleType = module.Results;
      //   
   }
}

但是,如果我有数百个模块怎么办?这意味着将为每个模块执行一个单独的用于接收结果记录的SQL查询显然,这会对服务器造成压力,并显着降低应用程序速度。在Entity Framework中,答案非常简单-您可以指定其在查询中包括一组特定的结果。请参阅下面的修改后的代码,其中将仅执行一个SQL查询,该查询将包括每个模块和该模块的每个结果(合并为一个查询,实体框架将智能地显示在模型中),

using (var db = new DBEntities())
{
   var modules = db.Modules.Include(b => b.Results);
   foreach (var module in modules)
   {
       var moduleType = module.Results;
      //   
   }
}

5.不了解LINQ to SQL / Entity Frameworks如何转换查询


由于我们涉及到LINQ主题,因此我认为值得一提的是,如果代码在LINQ查询中,则代码执行的方式会有所不同。从高层次上讲,LINQ查询中的所有代码都使用表达式转换为SQL- 这似乎很明显,但是非常非常容易忘记您所处的上下文,并最终在代码库中引入问题。下面,我整理了一个列表来描述您可能遇到的一些典型障碍。

大多数方法调用将不起作用。

因此,假设您在下面的查询中使用冒号分隔所有模块的名称并捕获第二部分。

var modules = from m in db.Modules
              select m.Name.Split(':')[1];

在大多数LINQ提供程序中,您会遇到一个例外-Split方法没有SQL转换,某些方法可能受支持,例如,在日期中增加几天,但这全取决于您的提供程序。

那些可行的方法可能会产生出乎意料的结果...

采用下面的LINQ表达式(我不知道您为什么会在实践中这样做,但是请想象这是一个合理的要求)。

int modules = db.Modules.Sum(a => a.ID);

如果模块表中有任何行,它将为您提供标识符的总和。听起来不错!但是,如果您改为使用LINQ to Objects执行该操作,该怎么办?为此,我们可以在执行Sum方法之前将模块集合转换为列表。

int modules = db.Modules.ToList().Sum(a => a.ID);

震惊,恐怖-会完全一样!但是,如果模块表中没有行怎么办? LINQ to Objects返回0,并且Entity Framework / LINQ to SQL版本抛出InvalidOperationException,这表明它不能转换“ int?”。在“诠释” ...这样。这是因为当您在SQL中为空集执行SUM时,将返回NULL而不是0-因此,它会尝试返回可为null的int。如果遇到此类问题,请参考以下提示以解决此问题

知道何时只需要使用良好的旧SQL。

如果您正在执行一个非常复杂的请求,那么您的翻译请求可能最终看起来像吐出来的东西,一次又一次地被吞掉。不幸的是,我没有任何示例可以说明,但是从流行的观点来看,我真的很喜欢使用嵌套视图,这使代码维护成为一场噩梦。

此外,如果遇到任何性能瓶颈,则由于无法直接控制所生成的SQL,将很难修复它们。如果您或您的公司拥有SQL,则可以用SQL进行,也可以将其委派给数据库管理员!

6.四舍五入错误


现在的事情比前几段要简单一些,但我总是忘了它,最终遇到了令人不愉快的错误(如果与财务相关,那就是愤怒的鳍/基因主管)。

.NET Framework在Math类中称为Round的静态方法非常出色,该方法采用数字值并将其四舍五入到指定的小数位。它在大多数情况下都能正常运行,但是当您尝试将2.25舍入到小数点后第一位时该怎么办?我假设您可能希望它会四舍五入到2.3-这就是我们都习惯的,对吧?嗯,实际上,事实证明.NET使用银行取整将给定的示例四舍五入为2.2!这是由于以下事实:如果银行家的数字位于“中点”,则四舍五入到最接近的偶数。幸运的是,这可以在Math.Round方法中轻松覆盖。

Math.Round(2.25,1, MidpointRounding.AwayFromZero)

7.可怕的“ DBNull”类


这可能会给某些人带来不愉快的记忆-ORM向我们隐藏了这种污秽,但是如果您深入研究裸ADO.NET(SqlDataReader之类)的世界,则会遇到DBNull.Value。

我不是100%知道为什么数据库中的NULL值按以下方式处理(如果您知道,请在下面注释!),但是Microsoft决定为它们提供特殊类型的DBNull(带有静态字段Value)。我可以提供其中一个优点-访问数据库字段NULL时,您不会遇到任何不愉快的NullReferenceException。但是,您不仅应该支持检查NULL值的第二种方法(这种方法很容易忘记,可能导致严重的错误),而且还会丢失C#的任何出色功能,这些功能可以帮助您处理null。可能如此简单

reader.GetString(0) ?? "NULL";

最终变成...

reader.GetString(0) != DBNull.Value ? reader.GetString(0) : "NULL";

啊。

注意


这些只是我在.NET中遇到的一些不平凡的“耙子”-如果您了解更多,我想在下面与您联系。



ASP.NET Core:



All Articles