C# 告别FirstOrDefault

news2025/1/17 1:18:18

一、开篇: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.coloradmin.cn/o/2277783.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

天机学堂2-高并发优化

day04-高并发优化 方案选择 实现了学习计划和学习进度的统计功能。特别是学习进度部分&#xff0c;为了更精确的记录用户上一次播放的进度&#xff0c;我们采用的方案是&#xff1a;前端每隔15秒就发起一次请求&#xff0c;将播放记录写入数据库。 在并发较高的情况下&#xf…

ROS2 准备工作(虚拟机安装,Ubuntu安装,ROS2系统安装)

准备工作 虚拟机安装 大家可以自行去安装VMware链接&#xff1a;https://pan.baidu.com/s/1KcN1I9FN--Sp1bUsjKqWVA?pwd6666 提取码&#xff1a;6666(提供者&#xff1a;零基础编程入门教程) 教程&#xff1a;【【2025最新版】VMware虚拟机安装教程&#xff0c;手把手教你免…

在一个地方待多久才会改变ip属地

‌在当今数字化时代&#xff0c;IP地址作为网络世界的“门牌号”&#xff0c;不仅承载着设备连接互联网的身份信息&#xff0c;还常常与地理位置相关联。随着人们频繁地迁徙、旅行或在不同地点工作&#xff0c;一个自然而然的问题浮现在许多人心头&#xff1a;究竟在一个地方待…

CCLINKIE转ModbusTCP网关,助机器人“掀起”工业智能的“惊涛骇浪”

以下是一个稳联技术CCLINKIE转ModbusTCP网关&#xff08;WL-CCL-MTCP&#xff09;连接三菱PLC与机器人的配置案例&#xff1a;设备与软件准备设备&#xff1a;稳联技术WL-CCL-MTCP网关、三菱FX5UPLC、支持ModbusTCP协议的机器人、网线等。 稳联技术ModbusTCP转CCLINKIE网关&…

QT在 MacOS X上,如何检测点击程序坞中的Dock图标

最近在开发MacOS的qt应用&#xff0c;在做到最小化系统托盘功能时&#xff0c;发现关闭窗口后再次点击程序坞中的Dock图标不能将主界面再显示出来。查询里很多资料&#xff0c;发现是QT自身的问题&#xff0c;没有做相关的点击Dock图标的处理。 于是我参考了国内和国外的这两篇…

langchain4j执行源码分析

要做大模型应用&#xff0c;不可避免会接触到langchain&#xff0c;但是langchain本身使用py实现&#xff0c;对于java用户上手体验不是很友好。继而出现了java版的langchain&#xff0c;即langchain-4j。这里我们用脑图分析一下其执行源码。

【案例81】NMC调用导致数据库的效率问题

问题现象 客户在使用NC系统时&#xff0c;发现系统特别卡顿。需要紧急排查。 问题分析 排查NMC发现&#xff0c;所有的线程都处于执行SQL层面&#xff0c;说明数据库当前出现了异常。查看数据库资源状态发现&#xff0c;Oracle相关进程CPU利用率达到了100%。 查看现在数据库…

PyTorch框架——基于深度学习YOLOv5神经网络水果蔬菜检测识别系统

基于深度学习YOLOv5神经网络水果蔬菜检测识别系统&#xff0c;其能识别的水果蔬菜有15种&#xff0c;# 水果的种类 names: [黑葡萄, 绿葡萄, 樱桃, 西瓜, 龙眼, 香蕉, 芒果, 菠萝, 柚子, 草莓, 苹果, 柑橘, 火龙果, 梨子, 花生, 黄瓜, 土豆, 大蒜, 茄子, 白萝卜, 辣椒, 胡萝卜,…

DFT可测性设置与Tetramax测试笔记

1 DFT 1.1 DFT类型 1、扫描链&#xff08;SCAN&#xff09;&#xff1a; 扫描路径法是一种针对时序电路芯片的DFT方案.其基本原理是时序电路可以模型化为一个组合电路网络和带触发器(Flip-Flop&#xff0c;简称FF)的时序电路网络的反馈。 Scan 包括两个步骤&#xff0c;scan…

分布式ID的实现方案

1. 什么是分布式ID ​ 对于低访问量的系统来说&#xff0c;无需对数据库进行分库分表&#xff0c;单库单表完全可以应对&#xff0c;但是随着系统访问量的上升&#xff0c;单表单库的访问压力逐渐增大&#xff0c;这时候就需要采用分库分表的方案&#xff0c;来缓解压力。 ​…

28.找出字符串中第一个匹配项的下标【力扣】KMP前缀表 ≈ find() 函数、暴力解法

class Solution { public: //得到前缀表void getNext(int *next,string needle){int j0;for(int i1;i<needle.size();i){while(j>0 && needle[j]!needle[i]) jnext[j-1];//**j>0**>j0是出口if(needle[i]needle[j]) j;next[i]j;//若写入if中&#xff0c;则该…

当自动包布机遇上Profinet转ModbusTCP网关,“妙啊”,工业智能“前景无限

在自动化控制技术日新月异的当下&#xff0c;Profinet与ModbusTCP这两种协议在工业通信领域占据着举足轻重的地位。ModbusTCP是基于以太网的串行通信协议&#xff0c;而Profinet则是依托工业以太网的现场总线协议。它们在数据传输速度、实时性表现以及兼容性等方面各具特色。不…

ADC(Analog-to-digital converter)模拟-数字转换器

ADC简介 ADC&#xff08;Analog-to-Digital Converter&#xff09;&#xff0c;即模拟-数字转换器&#xff0c;是一种将模拟信号转换成数字信号的电子设备。它在现代电子系统中扮演着至关重要的角色&#xff0c;广泛应用于传感器信号处理、通信系统、医疗设备、工业自动化等多…

Uniapp判断设备是安卓还是 iOS,并调用不同的方法

在 UniApp 中&#xff0c;可以通过 uni.getSystemInfoSync() 方法来获取设备信息&#xff0c;然后根据系统类型判断当前设备是安卓还是 iOS&#xff0c;并调用不同的方法。 示例代码 export default {onLoad() {this.checkPlatform();},methods: {checkPlatform() {// 获取系…

TP4056锂电池充放电芯片教程文章详解·内置驱动电路资源!!!

目录 TP4056工作原理 TP4056引脚详解 TP4056驱动电路图 锂电池充放电板子绘制 编写不易&#xff0c;仅供学习&#xff0c;感谢理解。 TP4056工作原理 TP4056是专门为单节锂电池或锂聚合物电池设计的线性充电器&#xff0c;充电电流可以用外部电阻设定&#xff0c;最大充电…

平滑算法 效果比较

目录 高斯平滑 效果对比 移动平均效果比较: 高斯平滑 效果对比 右边两个参数是1.5 2 代码: smooth_demo.py import numpy as np import cv2 from scipy.ndimage import gaussian_filter1ddef gaussian_smooth_array(arr, sigma):smoothed_arr = gaussian_filter1d(arr, s…

Jenkins-简介/安装!

一. 关于持续集成&#xff1a; 持续集成(CI ) [ Continuous Integration ]&#xff0c;通俗来讲&#xff0c;就是一个能监控版本控制系统变化的工具&#xff0c;可以自动编译和测试集成的应用程序。出现问题&#xff0c;能够及时的通知相应人员。持续集成是一种思维工具集&…

Flutter中Get.snackbar避免重复显示的实现

在pubspec.yaml中引入依赖框架。 #GetX依赖注解get: ^4.6.5创建一个SnackBarManager管理类去管理每个提示框。 import package:get/get.dart; import package:flutter/material.dart;class SnackBarManager {factory SnackBarManager() > instance;static final SnackBarMa…

c#删除文件和目录到回收站

之前在c上遇到过这个问题&#xff0c;折腾许久才解决了&#xff0c;这次在c#上再次遇到这个问题&#xff0c;不过似乎容易了一些&#xff0c;亲测代码如下&#xff0c;两种删除方式都写在代码中了。 直接上完整代码&#xff1a; using Microsoft.VisualBasic.FileIO; using Sy…

微信小程序集成Vant Weapp移动端开发的框架

什么是Vant Weapp Vant 是一个轻量、可靠的移动端组件库&#xff0c;于 2017 年开源。 目前 Vant 官方提供了 Vue 2 版本、Vue 3 版本和微信小程序版本&#xff0c;并由社区团队维护 React 版本和支付宝小程序版本。 官网地睛&#xff1a;介绍 - Vant Weapp (vant-ui.gith…