找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1001

积分

0

好友

127

主题
发表于 昨天 02:52 | 查看: 1| 回复: 0

本次复习包含12道编程题目,旨在帮助你系统性地回顾和巩固 LINQ 的核心概念、常见陷阱及工程应用模式。题目按难度递进,覆盖了从基础理解到复杂建模的各个层面。

题目数量:12 题
难度分布

  • 热身理解题:3 题
  • 核心建模题:6 题
  • 综合工程题:3 题

一、热身理解题

① 延迟执行 + 副作用理解

var logs = new List<string>();

var query = users
    .Where(u =>
    {
        logs.Add($"Checking {u.Name}");
        return u.Age >= 30;
    });

logs.Add("Before enumerate");

foreach (var u in query)
{
}

logs.Add("After enumerate");

请思考以下问题:

  1. logs 的最终内容顺序是什么?
  2. 如果删除 foreach 循环,这段代码还会往 logs 列表里添加内容吗?
  3. 这段代码在真实工程项目中是否合理?为什么?

✅ 标准答案

Before enumerate
Checking A
Checking B
...
After enumerate
  • Where 操作符遵循 延迟执行 原则。
  • 副作用(这里指添加日志)发生在 枚举阶段
  • 没有枚举操作,Lambda 表达式根本不会执行。

工程判断
不合理

  • LINQ 的 Lambda 表达式 不应该包含副作用,如写日志、修改外部状态等。
  • 日志记录或状态修改等操作,应明确放在 foreach 循环内或其它流程控制语句中。

② 多次枚举的隐性成本

var query = users.Where(u => u.Age > 18);

bool hasAdult = query.Any();
int count = query.Count();

请思考以下问题:

  1. Where 条件会被计算执行几次?
  2. 如果 users 来源于数据库查询,这段代码存在什么风险?
  3. 如何改写代码使其更安全?

✅ 标准答案

  1. 两次。
  2. 会导致两次独立的数据库查询,增加不必要的 IO 开销和网络往返,可能带来性能问题和数据不一致的风险。
  3. 通过 ToList()ToArray() 将结果固化,避免多次枚举:
var list = users.Where(u => u.Age >= 30).ToList();
var hasAny = list.Any();
var count = list.Count;

要点

多次枚举 = 多次 IO / 多次计算


③ IEnumerable vs List 的边界

IEnumerable<User> q = users.Where(u => u.IsActive);
var list = q.ToList();

请思考以下问题:

  1. 哪一行代码真正触发了查询执行?
  2. 如果在执行 ToList() 之后修改了原始的 users 集合,list 中的内容会随之变化吗?
  3. 如果调整 q 这行代码定义的顺序(比如先定义 q,后修改 users),会影响最终 list 的结果吗?

✅ 标准答案

  • ToList()立即执行查询并固化结果
  • list 不会变化,因为它已经是独立于源集合 users 的一个新列表快照。
  • 会影响。因为 q 是延迟执行的查询定义,在调用 ToList() 时才会基于当前的 users 状态进行计算。这种与基础与综合思想相关的“定义与执行分离”特性,是理解 LINQ 延迟执行的关键。

二、核心建模题(中级 LINQ 的应用核心)

④ XML 日志分析

给定 XML 结构:

<logs>
<log level="Info" module="Auth" />
<log level="Error" module="DB" />
<log level="Error" module="Auth" />
<log level="Warn" module="API" />
</logs>

假设你已将其解析为以下对象列表:

class LogEntry
{
    public string Level { get; set; }
    public string Module { get; set; }
}

要求:

  1. Level 分组。
  2. 统计每个 Level 出现的数量。
  3. 标记该 Level 组内是否存在 Error 级别的日志。
  4. 输出结构为 { Level, Count, HasError } 的匿名对象列表。
var query = logEntries.GroupBy(l => l.Level)
            .Select(g => new {
                      Level = g.Key,
                      Count = g.Count(),
                      HasError = g.Key == “Error”
                    }).ToList();

⑤ 配置文件合并(Union / DistinctBy)

class ConfigItem
{
    public string Key { get; set; }
    public string Value { get; set; }
}
  • 本地配置:localConfigs
  • 远程配置:remoteConfigs

要求:

  1. 合并两份配置列表。
  2. Key 作为唯一标识符。
  3. 本地配置优先(即 Key 冲突时,采用本地配置的值)。
  4. 输出最终去重后的配置列表。
// 1. 顺序至关重要:把“优先”的集合放在连接操作的前面
var finalConfigs = localConfigs
    .Concat(remoteConfigs)       // 先把两个列表连起来,本地在前,远程在后
    .DistinctBy(c => c.Key)      // 遇到重复的 Key,保留先看到的(即本地的)
    .ToList();

⑥ 权限系统建模(SelectMany)

class Role
{
    public string Name { get; set; }
    public List<string> Permissions { get; set; }
}

要求:

  1. 获取所有角色拥有的、去重后的权限列表。
  2. 按权限名称排序。
  3. 输出 List<string>
var permissionList = roles.SelectMany(r=>r.Permissions).Distinct().OrderBy(p=>p).ToList();

⑦ 操作审计流(TakeWhile / SkipWhile)

给定按时间排序的操作记录:

class Audit
{
    public DateTime Time { get; set; }
    public bool IsValid { get; set; }
}

要求:

  1. 获取连续合法的操作序列(直到遇到第一个非法操作为止)。
  2. 跳过所有合法操作,获取剩余部分(从第一个非法操作开始及之后的所有记录)。
  3. 思考:如果使用 Where 操作符来实现,会有什么不同?

1. 取「连续合法」的操作(直到第一个非法)

var validSequence = audits.TakeWhile(a => a.IsValid);
  • TakeWhile
    • 第一个元素开始 判断。
    • 条件第一次失败就整体停止,后续元素即使满足条件也不再包含。
  • 非常适合:
    • 启动流程检查
    • 操作校验链
    • 审计回放流

2. 跳过所有合法操作,取剩余非法部分

var invalidTail = audits.SkipWhile(a => a.IsValid);
  • 跳过「前缀合法区」。
  • 返回 第一个非法操作 + 之后的所有操作

3. 如果用 Where 会怎样?

audits.Where(a => a.IsValid);

❌ 不等价,核心区别:

TakeWhile / SkipWhile Where
顺序语义,依赖于集合的遍历顺序 无顺序语义,进行全局筛选
遇错即停(TakeWhile),或跳过前缀(SkipWhile) 全表扫描,返回所有符合条件的元素
表达“阶段”、“前缀”等流程概念 表达“筛选”集合的概念

⚠️ TakeWhile / SkipWhile流程算子Where集合算子


⑧ 日志分页(真实分页)

class Log
{
    public DateTime Time { get; set; }
    public string Message { get; set; }
}

要求:

  1. 按时间倒序排列。
  2. 实现获取第 pageIndex 页(从0开始),每页 pageSize 条记录的分页查询。
  3. 思考:分页前为什么必须进行排序?
var page = logs
    .OrderByDescending(l => l.Time)
    .Skip(pageIndex * pageSize)
    .Take(pageSize)
    .ToList();

为什么必须先排序

❗ 如果顺序不稳定:

  • 第 1 页和第 2 页的数据 可能重复或丢失
  • 用户界面翻页时会出现数据“闪烁”或错乱。
    👉 分页的前提是结果集拥有稳定、可预测的顺序。

排序是分页逻辑不可或缺的一部分,不是附加条件。


⑨ 业务异常检测(Any / All)

class ServiceStatus
{
    public string Service { get; set; }
    public bool IsHealthy { get; set; }
}

要求:

  1. 判断是否存在不健康的服务。
  2. 判断是否全部服务都健康。
  3. 思考:AnyAll 操作符的“短路”行为在此场景下的意义。
bool hasUnhealthy = statuses.Any(s => !s.IsHealthy);
bool allHealthy = statuses.All(s => s.IsHealthy);
  • Any:遇到 第一个 满足条件(true)的元素即返回 true
  • All:遇到 第一个 不满足条件(false)的元素即返回 false

⚠️ 工程意义

  • 监控检查:快速发现异常。
  • 告警触发:无需遍历完所有数据即可决策。
  • 数据库 Exists 查询:可高效转换为 EXISTS 语句。

👉 在只需判断“是否存在”的场景下,使用 AnyAllCount() > 0Count() == total 更快、更安全(避免了不必要的计数)。


三、综合工程题

⑩ 模块使用统计(GroupBy + 聚合)

class ModuleUsage
{
    public string Module { get; set; }
    public int Duration { get; set; }
}

要求:

  1. Module 分组。
  2. 统计每个模块的总使用时长。
  3. 统计每个模块的调用次数。
  4. 标记该模块是否存在单次调用时长超过 1000ms 的记录。
  5. 输出可直接绑定到 UI 视图的扁平化 ViewModel 列表。

✅ 标准答案

var vm = usages
    .GroupBy(u => u.Module)
    .Select(g => new ModuleUsageVm
    {
        Module = g.Key,
        TotalDuration = g.Sum(x => x.Duration),
        CallCount = g.Count(),
        HasLongCall = g.Any(x => x.Duration > 1000)
    })
    .ToList();
  • GroupBy 构建数据结构
  • 聚合函数(Sum, Count, Any)生成业务结论
  • 输出扁平化的 ViewModel,而不是 IGrouping 对象。

UI 视图应该绑定结论数据,而不是原始的分组结构。


⑪ 配置变更差异分析(Except / Intersect)

假设有以下配置类:

class Config
{
    public string Key { get; set; }
    public string Value { get; set; }
}

1. 新增了哪些配置?

var added = newConfigs
    .Select(c => c.Key)
    .Except(oldConfigs.Select(c => c.Key));

2. 删除了哪些配置?

var removed = oldConfigs
    .Select(c => c.Key)
    .Except(newConfigs.Select(c => c.Key));

3. Key 相同但 Value 发生了改变?

var changed =
    from oldC in oldConfigs
    join newC in newConfigs on oldC.Key equals newC.Key
    where oldC.Value != newC.Value
    select new { oldC.Key, oldC.Value, newC.Value };

4. 哪些地方不能只靠 Except

Except 只能判断某个元素 “是否存在” 于另一个集合中。
它无法判断:

  • 对象属性的具体变化(如 Value 字段的修改)。
  • 复杂对象的状态演进
  • 差异的具体来源和内容

Except 看“有没有”,Join 看“变没变”。


⑫ LINQ 设计题

问题:

在一个 WPF 项目中,你需要从 ObservableCollection<Data> 派生出多个用于显示统计信息的 ViewModel。

  • 哪些场景下适合使用 LINQ?
  • 哪些场景下必须使用 ToList()ToArray()
  • 哪些场景下你会坚决避免使用 LINQ?

请写出你的判断理由,而非具体代码。

✔ 适合使用 LINQ 的地方

  • 派生 ViewModel:对源数据进行筛选、转换以生成显示数据。
  • 统计计算:求和、计数、平均值、分组等只读聚合操作。
  • 数据筛选与投影:根据条件选择特定数据或转换其形状。
  • 核心原则:不改动原始数据声明式只读计算

✔ 必须使用 ToList()ToArray() 的地方

  • 结果被多次使用:避免因延迟执行导致的多次计算或查询。
  • 作为 UI 控件的绑定源(如 ItemsSource):WPF/Silverlight 等框架通常需要稳定的集合(如 List<T>)而非 IEnumerable<T> 进行绑定,且能防止源集合变化导致绑定失效。
  • 固化快照:需要在某个时间点保存数据的完整状态,防止后续对源集合的修改影响当前视图。

关于集合操作的最佳实践与陷阱,你可以在我们的技术文档专区找到更多深度解析。

❌ 坚决避免使用 LINQ 的地方

  • 操作包含副作用:如修改全局状态、写入日志、发送网络请求等。
  • 复杂的条件分支(if/else)或流程控制:传统的 foreach 循环和 if 语句在逻辑清晰度上通常更胜一筹。
  • 实现状态机或复杂的多步骤流程:LINQ 是查询语言,不擅长描述有状态的流程。
  • 性能极度敏感的热点路径:虽然 LINQ 性能在大多数情况下足够好,但在纳秒级优化的场景下,手写循环可能仍有微优势(需经性能测评确认)。

⚠️ 一句话总原则:

LINQ 是用于数据查询和转换的“表达式”语言,不是用于描述任意业务“流程”的命令式语言。

希望这套C#/.Net领域的LINQ综合练习题能帮助你查漏补缺,将理论知识转化为解决实际工程问题的能力。如果你在练习中遇到任何问题或有独到的见解,欢迎到云栈社区与更多开发者交流讨论。




上一篇:Python面试高频易错点解析:从装饰器到GIL的10道核心题目
下一篇:C#中高效数据分页查询:LINQ的Take与Skip方法核心用法与性能解析
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-2-9 00:52 , Processed in 0.302974 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表