当前位置: 首页 > article >正文

C# 告别FirstOrDefault

一、开篇:FirstOrDefault 的 “江湖地位”

在 C# 编程的世界里,FirstOrDefault 可谓是一位 “常客”,被广大开发者频繁地运用在各种项目场景之中。无论是 Windows 窗体应用程序,需要从数据集中检索第一条记录,或是满足特定条件的关键数据;还是ASP.NET Web 应用程序,在处理来自数据库的海量信息时,精准地抓取单个结果;亦或是控制台应用程序,面对数据流、文件数据时,快速定位首行内容;乃至 WPF 应用程序,处理数据绑定、UI 元素相关数据时,它都能派上用场,帮我们轻松获取集合中的第一个元素。甚至在类库项目里,开发者们还常常将它封装在公共方法内,以便在不同项目中重复利用,足见其通用性与灵活性。但今天,咱们得静下心来,好好探讨一下,为何在某些情况下,我们需要和这位 “老友” 暂别,去寻觅更好的替代方案。

二、深入剖析 FirstOrDefault

2.1 基本用法回顾

FirstOrDefault 是 C# 中 Linq 的一个扩展方法,它的定义为:public static TSource FirstOrDefault(this IEnumerable source);,这个方法接受一个类型为IEnumerable的参数source,返回源序列中的第一个元素或默认值。

咱们来看一段简单的代码示例:

List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
int firstOrDefaultNumber = numbers.FirstOrDefault();

在上述代码中,numbers列表包含了多个整数元素,当我们调用FirstOrDefault方法时,它会按顺序遍历这个列表,由于列表不为空,便会返回第一个元素,也就是1。倘若我们将代码修改为:

List<int> emptyList = new List<int>();
int defaultNumber = emptyList.FirstOrDefault();

此时,emptyList为空列表,FirstOrDefault方法会直接返回int类型的默认值,也就是0。

再看一个字符串类型的例子:

List<string> names = new List<string> { "张三", "李四", "王五" };
string firstOrDefaultName = names.FirstOrDefault(s => s.StartsWith("王"));

这里,我们传入了一个 lambda 表达式作为条件,方法会在列表names中查找第一个以 “王” 开头的字符串,最终返回"王五"。若列表中不存在满足条件的元素,该方法就会返回string类型的默认值null。

2.2 原理拆解

从原理层面来讲,当我们调用FirstOrDefault方法时,它内部的执行逻辑是先检查序列是否为空。若为空,就直接返回对应类型的默认值;若不为空,则返回序列的第一个元素。

以一个简单的数组查找为例,假设我们有一个整数数组intArray:

int[] intArray = { 10, 20, 30, 40 };
int result = intArray.FirstOrDefault();

程序流程进入FirstOrDefault方法后,首先判断intArray是否为空,显然这里它不为空,于是直接返回第一个元素10。若将intArray定义为空数组:

int[] intArray = new int[0];
int result = intArray.FirstOrDefault();

此时,方法检测到数组为空,便返回int类型的默认值0。

三、那些年,FirstOrDefault 带来的 “坑”

3.1 返回值的模糊性

FirstOrDefault 最大的一个 “坑”,便是返回值的模糊性。当它返回默认值时,我们很难直观地判断究竟是因为没有找到符合条件的元素,还是真的找到了恰好与默认值相同的元素。

举个例子,假设我们有一个List类型的列表,用来存储学生的考试成绩,我们想要查找成绩为0分的学生:

List<int> scores = new List<int> { 0, 60, 80, 90 };
int result = scores.FirstOrDefault(s => s == 0);

这里,result的值为0,但我们无法仅凭这个返回值就确定是列表中的第一个学生恰好考了0分,还是根本没有考0分的学生,只是返回了int类型的默认值。这种模糊性在实际项目中,尤其是数据处理逻辑较为复杂时,极易引发错误,让开发者花费大量时间去排查问题根源。

3.2 引用与值类型的 “纠结”

对于引用类型和值类型,FirstOrDefault 的行为也存在一些令人 “纠结” 的地方。

当处理引用类型的元素时,比如List,其中Person是一个自定义的类:

class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

List<Person> people = new List<Person>
{
    new Person { Name = "张三", Age = 20 },
    new Person { Name = "李四", Age = 25 }
};
Person foundPerson = people.FirstOrDefault(p => p.Age > 22);

此时,foundPerson返回的是满足条件的Person对象的引用(如果找到的话)。但如果我们对这个返回的引用进行修改,就会直接影响到原列表中的元素,这可能会在不经意间破坏数据的一致性。

而对于值类型,例如List,当调用FirstOrDefault找到元素时,实际上返回的是元素值的一个副本。这意味着,对返回值进行操作,不会影响原列表中的元素,但同时也可能带来额外的性能开销,尤其是在处理大型结构体等复杂值类型时,频繁的复制操作会占用大量内存,降低程序效率。

3.3 隐藏的性能隐患

在一些看似简单的场景下,FirstOrDefault 还隐藏着性能隐患。

当我们面对复杂的数据结构,如多层嵌套的集合,或者大数据量的序列时,FirstOrDefault方法总是从序列的起始位置开始,逐个元素地进行条件判断,直至找到第一个满足条件的元素或者遍历完整个序列。

想象一下,我们有一个存储了海量用户数据的列表,要查找某个特定地区的第一个用户:

List<User> users = GetHugeUserList(); // 假设这是一个获取海量用户列表的方法
User targetUser = users.FirstOrDefault(u => u.Region == "特定地区");

在这个过程中,如果满足条件的用户位于列表末尾,或者根本不存在,那么FirstOrDefault方法就会进行大量不必要的迭代操作,白白浪费 CPU 资源,导致程序性能下降。特别是在对性能要求极高的实时系统、大数据处理场景中,这种性能损耗可能是致命的,使系统响应延迟,用户体验大打折扣。

四、替代方案 “群雄逐鹿”

4.1 SingleOrDefault 的 “特长”

SingleOrDefault 方法在特定场景下有着独特的优势。它的定义为:public static TSource SingleOrDefault(this IEnumerable source);,这个方法旨在返回序列中满足特定条件的唯一元素,如果序列为空或者包含多个满足条件的元素,它会返回默认值或者抛出异常。

与 FirstOrDefault 相比,当我们明确知道查询结果应该只有一个元素时,SingleOrDefault 能帮我们更严谨地处理这种情况。例如,在一个用户管理系统中,我们根据唯一的用户名去数据库查询对应的用户记录:

List<User> users = GetAllUsers(); // 假设这是获取所有用户的方法
User targetUser = users.SingleOrDefault(u => u.UserName == "特定用户名");
if (targetUser == null)
{
    Console.WriteLine("未找到该用户");
}
else
{
    // 对找到的用户进行操作
}

在上述代码中,如果没有找到匹配 “特定用户名” 的用户,SingleOrDefault会返回null;而如果找到了多个同名用户(这在用户名应唯一的场景下是异常情况),它会抛出InvalidOperationException异常,提示我们数据出现了问题,让开发者能及时发现并修复潜在的数据错误,相比 FirstOrDefault 返回值的模糊性,这无疑更加安全、可靠。

4.2 First 的 “果敢”

First 方法,其定义为:public static TSource First(this IEnumerable source);,它会直接返回序列的第一个元素,毫不拖泥带水。但要注意,如果序列为空,它会抛出InvalidOperationException异常。

在某些场景下,这种 “果敢” 反而能让代码更加清晰。比如,我们有一个固定的列表,用来存储系统的配置项,且我们确定这个列表不为空,此时只需获取头部的配置项:

List<ConfigItem> configList = GetSystemConfigs(); // 获取系统配置项列表
ConfigItem firstConfig = configList.First();
// 使用firstConfig进行后续操作

这里使用 First 方法,明确表达了我们对列表非空的预期,同时避免了 FirstOrDefault 可能带来的默认值混淆问题,让代码阅读者一眼就能明白开发者的意图,提升代码的可读性。

4.3 自定义扩展方法 “定制化出击”

除了上述内置方法,开发者还可以根据项目的独特需求,自定义扩展方法来替代 FirstOrDefault。

假设我们的项目经常需要在查找元素时记录详细的日志,以便排查问题,我们可以创建一个如下的扩展方法:

public static class MyLinqExtensions
{
    public static TSource FirstOrDefaultWithLog<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate, Action<string> logAction)
    {
        TSource result = default;
        try
        {
            result = source.FirstOrDefault(predicate);
            if (result == null)
            {
                logAction($"未找到满足条件 {predicate.Method.Name} 的元素");
            }
        }
        catch (Exception ex)
        {
            logAction($"查找元素时出错: {ex.Message}");
        }
        return result;
    }
}

使用时,我们可以这样调用:

List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
int result = numbers.FirstOrDefaultWithLog(n => n > 10, Console.WriteLine);

这样,当未找到满足条件的元素或者出现异常时,都会在控制台输出详细的日志信息,方便我们调试。

又或者,我们需要一个更加智能的默认值设定,根据不同情况返回不同的默认值,而非固定类型的默认值,代码示例如下:

public static class MyLinqExtensions
{
    public static TSource FirstOrDefaultCustom<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate, Func<TSource> customDefaultValueProvider)
    {
        TSource result = source.FirstOrDefault(predicate);
        if (result == null)
        {
            result = customDefaultValueProvider();
        }
        return result;
    }
}

调用示例:

List<string> names = new List<string>();
string customDefault = "未找到合适名称";
string name = names.FirstOrDefaultCustom(s => s.StartsWith("X"), () => customDefault);

通过这种自定义扩展方法,我们可以精准地满足项目的个性化需求,让代码在面对复杂业务逻辑时更加游刃有余。

五、实战场景抉择:如何 “选贤任能”

5.1 数据库查询场景

在数据库查询场景中,FirstOrDefault 常常被用于获取单条记录。例如,我们使用 Entity Framework Core 从数据库中查询一个用户信息:

using (var context = new YourDbContext())
{
    User user = context.Users.FirstOrDefault(u => u.UserId == 1);
    if (user!= null)
    {
        // 对查询到的用户进行操作
    }
    else
    {
        // 处理未找到用户的情况
    }
}

这里,如果数据库中存在UserId为1的用户,就能顺利获取到该用户信息;若不存在,返回null,避免了空引用异常。

但当我们对数据的唯一性有严格要求时,比如查询唯一的订单号对应的订单详情,此时 SingleOrDefault 更为合适:

using (var context = new YourDbContext())
{
    Order order = context.Orders.SingleOrDefault(o => o.OrderNumber == "20230808001");
    if (order == null)
    {
        Console.WriteLine("未找到该订单");
    }
    else if (context.Orders.Count(o => o.OrderNumber == "20230808001") > 1)
    {
        Console.WriteLine("订单号不唯一,数据有误");
    }
    else
    {
        // 处理查询到的唯一订单
    }
}

这段代码不仅能处理订单不存在的情况,当出现多个相同订单号时,还会抛出异常,提醒开发者数据出现了重复,保证了数据的一致性和准确性。

5.2 集合数据处理

在处理集合数据时,选择合适的方法至关重要。

假设我们有一个小型的固定集合,用来存储系统的初始配置参数:

List<ConfigParameter> configParameters = new List<ConfigParameter>
{
    new ConfigParameter { Name = "Param1", Value = "Value1" },
    new ConfigParameter { Name = "Param2", Value = "Value2" }
};
ConfigParameter firstParam = configParameters.First();

由于我们明确知道集合不为空且只需获取第一个参数,使用 First 方法简洁高效,还避免了 FirstOrDefault 返回默认值可能带来的混淆。

然而,当面对复杂多变的集合,比如一个电商系统中的商品列表,需要筛选出特定品牌且价格最低的商品:

List<Product> products = GetAllProducts(); // 假设这是获取所有商品的方法
Product targetProduct = null;
if (products.Any(p => p.Brand == "TargetBrand"))
{
    targetProduct = products.Where(p => p.Brand == "TargetBrand").OrderBy(p => p.Price).FirstOrDefault();
}
if (targetProduct!= null)
{
    // 对找到的商品进行推荐、展示等操作
}
else
{
    // 处理未找到合适商品的情况
}

这里先通过Any方法判断是否存在目标品牌的商品,若存在,再结合Where筛选、OrderBy排序后用 FirstOrDefault 获取符合条件的商品。若直接用 First,当不存在目标品牌商品时会抛出异常;若用 SingleOrDefault,在有多个相同最低价商品时会报错,均不符合业务需求。所以,要依据数据特性、业务逻辑,权衡三者的利弊,做出最优选择。

六、总结:编程路上的 “迭代升级”

FirstOrDefault 在 C# 编程的历史长河中,确实为我们提供了诸多便利,凭借其简洁的语法,让开发者能迅速从集合中抓取元素,节省了大量开发时间。然而,随着项目复杂度的攀升、对代码质量和性能要求的愈发严苛,它的一些弊端逐渐显现,如返回值的模糊性容易引入难以察觉的逻辑错误,在处理不同类型数据时的 “暗坑”,以及隐藏的性能瓶颈,都可能成为项目前进路上的 “绊脚石”。

庆幸的是,C# 丰富的语言特性为我们准备了诸如 SingleOrDefault、First 等替代方法,还有自定义扩展方法这一 “利器”,让我们能够依据项目的独特需求,量体裁衣,精准优化代码。在编程的漫漫征途中,没有一成不变的 “最优解”,唯有紧跟技术发展步伐,不断反思、持续优化,才能让我们的代码在不同场景下都能高效运行。希望各位开发者在日后的编程实践中,能深入理解这些方法的精妙之处,灵活抉择,让代码世界更加 “精彩”。


http://www.kler.cn/a/500438.html

相关文章:

  • 【Rust】切片类型
  • Objective-C语言的语法
  • Python学习(三)基础入门(数据类型、变量、条件判断、模式匹配、循环)
  • 【excel】VBA简介(Visual Basic for Applications)
  • NLTK分词以及处理方法
  • 使用sqlplus的easy connect时如何指定是链接到shared server还是dedicated process
  • 轻松高效拿捏C语言02Hello World
  • zerotier已配置但ip连不上?
  • PHP多功能投票小程序源码
  • 代码随想录day26 | leetcode 134.加油站 135.分发糖果 860.柠檬水找零 406.根据身高重建队列
  • 基于java的餐厅点餐系统微信小程序ssm+论文源码调试讲解
  • Tomcat(133)Tomcat的SSL会话缓存故障排除
  • HTTP 范围Range请求
  • SQL分类与数据类型整理
  • Erlang语言的正则表达式
  • 自动化测试框架搭建-接口数据结构设计
  • NLP 基础理论和工具使用
  • C++实现设计模式---工厂方法模式 (Factory Method)
  • 科技快讯 | 抖音治理AI造假地震图片;投影仪也玩三折叠;京东发布“AI京医”大模型
  • XML 解析器:深入解析与高效应用
  • SpringBoot错误码国际化
  • 【源码解析】Java NIO 包中的 ByteBuffer
  • unittest VS pytest
  • 华纳云:在centos7中tomcat内存怎么设置?
  • Win10微调大语言模型ChatGLM2-6B
  • 测试ip端口-telnet开启与使用