2.9 Linq


文档摘要

2.9 Linq Unity3D C# 脚本编程:2.9 LINQ 详解与实践 引言 2.9.1 LINQ 概述 LINQ (Language Integrated Query),即语言集成查询,是 .NET Framework 提供的一项强大技术,它允许开发者使用统一的查询语法来访问和操作各种数据源。这些数据源可以包括: 集合 (Collections): 例如 , , 等。 数据库 (Databases): 通过 LINQ to SQL 或 Entity Framework 等技术。 XML 文档 (XML Documents): 通过 LINQ to XML。 其他数据源: 只要数据源实现了 或 接口,就可以使用 LINQ 进行查询。

2.9 Linq

Unity3D C# 脚本编程:2.9 LINQ 详解与实践

引言

2.9.1 LINQ 概述

LINQ (Language Integrated Query),即语言集成查询,是 .NET Framework 提供的一项强大技术,它允许开发者使用统一的查询语法来访问和操作各种数据源。这些数据源可以包括:

  • 集合 (Collections): 例如 List<T>, Array, Dictionary<TKey, TValue> 等。

  • 数据库 (Databases): 通过 LINQ to SQL 或 Entity Framework 等技术。

  • XML 文档 (XML Documents): 通过 LINQ to XML。

  • 其他数据源: 只要数据源实现了 IEnumerable<T>IQueryable<T> 接口,就可以使用 LINQ 进行查询。

LINQ 的优势:

  • 简洁性: LINQ 使用声明式语法,让代码更易读、易维护。您只需描述想要查询什么,而无需关注底层数据访问的细节。

  • 类型安全: LINQ 是强类型的,在编译时就能检查类型错误,减少运行时错误。

  • 统一性: 使用相同的查询语法操作不同类型的数据源,降低学习成本。

  • 强大的查询能力: LINQ 提供了丰富的查询操作符,例如筛选、排序、分组、连接等,可以满足各种复杂的数据处理需求。

  • 延迟执行: LINQ 查询通常是延迟执行的,只有在真正需要结果时才会执行查询操作,这有助于提高性能。

2.9.2 LINQ 的核心组件

LINQ 主要由以下几个核心组件构成:

  • LINQ 提供程序 (LINQ Providers): 负责将 LINQ 查询转换为特定数据源的查询语言。例如,LINQ to Objects 用于查询内存中的对象集合,LINQ to SQL 用于查询 SQL 数据库。在 Unity3D 中,我们主要使用 LINQ to Objects 来处理游戏对象和数据集合。

  • 扩展方法 (Extension Methods): LINQ 的查询操作符(例如 Where, Select, OrderBy 等)都是通过扩展方法来实现的。这些扩展方法为 IEnumerable<T>IQueryable<T> 接口提供了额外的功能。

  • Lambda 表达式 (Lambda Expressions): Lambda 表达式是一种简洁的匿名函数,常用于 LINQ 查询中,作为查询操作符的参数,用于定义查询条件或转换逻辑。

2.9.3 LINQ 查询语法和方法语法

LINQ 提供了两种主要的查询语法:

  • 查询语法 (Query Syntax): 类似于 SQL 查询语句,使用 from, where, select, orderby 等关键字。查询语法更接近自然语言,易于理解。

  • 方法语法 (Method Syntax): 使用扩展方法链式调用,例如 collection.Where(...).Select(...).OrderBy(...)。方法语法更加灵活,可以实现更复杂的查询逻辑。

在 Unity3D 中,两种语法都可以使用,选择哪种语法通常取决于个人偏好和代码的可读性。

2.9.4 常用 LINQ 操作符详解与实践 (Unity3D)

以下将详细介绍一些常用的 LINQ 操作符,并结合 Unity3D 的实际应用场景,提供代码示例和解释。

2.9.4.1 筛选操作符 (Filtering Operators)

  • Where(): 根据指定的条件筛选集合中的元素。

    代码示例 (Unity): 查找场景中所有标签为 "Enemy" 的游戏对象。

    using UnityEngine; using System.Linq; public class LinqExample_Where : MonoBehaviour { void Start() { GameObject[] allGameObjects = FindObjectsOfType<GameObject>(); // 使用查询语法 var enemyObjectsQuery = from obj in allGameObjects where obj.tag == "Enemy" select obj; // 使用方法语法 var enemyObjectsMethod = allGameObjects.Where(obj => obj.tag == "Enemy"); Debug.Log("使用查询语法找到的 Enemy 对象:"); foreach (var enemy in enemyObjectsQuery) { Debug.Log(enemy.name); } Debug.Log("使用方法语法找到的 Enemy 对象:"); foreach (var enemy in enemyObjectsMethod) { Debug.Log(enemy.name); } } }

    代码解释:

    • FindObjectsOfType<GameObject>() 获取场景中所有激活的 GameObject 对象数组。

    • Where(obj => obj.tag == "Enemy") 使用 Lambda 表达式 obj => obj.tag == "Enemy" 作为条件,筛选出 tag 属性为 "Enemy" 的 GameObject 对象。

    • 两种语法 (enemyObjectsQueryenemyObjectsMethod) 都实现了相同的筛选功能。

  • OfType<T>(): 筛选集合中指定类型的元素。

    代码示例 (Unity): 查找场景中所有 Light 组件。

    using UnityEngine; using System.Linq; public class LinqExample_OfType : MonoBehaviour { void Start() { GameObject[] allGameObjects = FindObjectsOfType<GameObject>(); // 使用 OfType 筛选 Light 组件 var lightComponents = allGameObjects.SelectMany(obj => obj.GetComponents<Component>()).OfType<Light>(); Debug.Log("场景中的 Light 组件:"); foreach (var light in lightComponents) { Debug.Log(light.gameObject.name + " - " + light.name); } } }

    代码解释:

    • SelectMany(obj => obj.GetComponents<Component>()) 首先使用 SelectMany 展开每个 GameObject 的所有组件到一个扁平的集合中。

    • OfType<Light>() 然后使用 OfType<Light>() 筛选出类型为 Light 的组件。

2.9.4.2 投影操作符 (Projection Operators)

  • Select<TResult>(): 将集合中的每个元素投影到新的形式。

    代码示例 (Unity): 获取场景中所有游戏对象的名称列表。

    using UnityEngine; using System.Linq; public class LinqExample_Select : MonoBehaviour { void Start() { GameObject[] allGameObjects = FindObjectsOfType<GameObject>(); // 使用 Select 获取游戏对象名称列表 var gameObjectNames = allGameObjects.Select(obj => obj.name); Debug.Log("游戏对象名称列表:"); foreach (var name in gameObjectNames) { Debug.Log(name); } } }

    代码解释:

    • Select(obj => obj.name) 使用 Lambda 表达式 obj => obj.name 将每个 GameObject 对象投影到其 name 属性。
  • SelectMany<TResult>(): 将集合中的每个元素投影到一个序列,并将所有序列扁平化为一个序列。

    代码示例 (Unity): 获取场景中所有游戏对象的所有子对象的名称列表。

    using UnityEngine; using System.Linq; public class LinqExample_SelectMany : MonoBehaviour { void Start() { GameObject[] allGameObjects = FindObjectsOfType<GameObject>(); // 使用 SelectMany 获取所有子对象的名称列表 var childObjectNames = allGameObjects.SelectMany(obj => obj.GetComponentsInChildren<Transform>()) .Select(transform => transform.gameObject.name); Debug.Log("所有子对象的名称列表:"); foreach (var name in childObjectNames) { Debug.Log(name); } } }

    代码解释:

    • SelectMany(obj => obj.GetComponentsInChildren<Transform>()) 使用 GetComponentsInChildren<Transform>() 获取每个 GameObject 及其所有子对象的 Transform 组件,并将这些 Transform 组件序列扁平化为一个序列。

    • Select(transform => transform.gameObject.name) 然后使用 Select 将每个 Transform 组件投影到其关联的 GameObject 的名称。

2.9.4.3 排序操作符 (Ordering Operators)

  • OrderBy(): 按升序对集合中的元素进行排序。

  • OrderByDescending(): 按降序对集合中的元素进行排序。

  • ThenBy() / ThenByDescending():OrderByOrderByDescending 之后,用于指定次要排序条件。

    代码示例 (Unity): 根据游戏对象的 X 坐标对场景中的游戏对象进行排序。

    using UnityEngine; using System.Linq; public class LinqExample_OrderBy : MonoBehaviour { void Start() { GameObject[] allGameObjects = FindObjectsOfType<GameObject>(); // 按 X 坐标升序排序 var sortedByXAscending = allGameObjects.OrderBy(obj => obj.transform.position.x); // 按 X 坐标降序排序 var sortedByXDescending = allGameObjects.OrderByDescending(obj => obj.transform.position.x); Debug.Log("按 X 坐标升序排序:"); foreach (var obj in sortedByXAscending) { Debug.Log(obj.name + " - X: " + obj.transform.position.x); } Debug.Log("按 X 坐标降序排序:"); foreach (var obj in sortedByXDescending) { Debug.Log(obj.name + " - X: " + obj.transform.position.x); } } }

    代码解释:

    • OrderBy(obj => obj.transform.position.x) 使用 Lambda 表达式 obj => obj.transform.position.x 指定排序的键为游戏对象的 X 坐标。

    • OrderByDescending() 用法类似,只是排序顺序为降序。

2.9.4.4 分组操作符 (Grouping Operators)

  • GroupBy<TKey>(): 根据指定的键将集合中的元素分组。

    代码示例 (Unity): 根据游戏对象的标签对场景中的游戏对象进行分组。

    using UnityEngine; using System.Linq; public class LinqExample_GroupBy : MonoBehaviour { void Start() { GameObject[] allGameObjects = FindObjectsOfType<GameObject>(); // 根据标签分组 var groupedByTag = allGameObjects.GroupBy(obj => obj.tag); Debug.Log("按标签分组:"); foreach (var group in groupedByTag) { Debug.Log($"标签: {group.Key}"); foreach (var obj in group) { Debug.Log($" - {obj.name}"); } } } }

    代码解释:

    • GroupBy(obj => obj.tag) 使用 Lambda 表达式 obj => obj.tag 指定分组的键为游戏对象的 tag 属性。

    • groupedByTag 的类型是 IEnumerable<IGrouping<string, GameObject>>,其中 IGrouping<string, GameObject> 表示一个分组,Key 属性为标签名称,分组中的元素可以通过迭代访问。

2.9.4.5 连接操作符 (Join Operators)

  • Join<TInner, TKey, TResult>(): 基于键值将两个集合的元素连接起来。

    代码示例 (Unity): 假设有两个列表,一个列表包含角色名称和 ID,另一个列表包含角色 ID 和武器名称,使用 Join 将这两个列表连接起来,得到角色名称和武器名称的组合。

    using UnityEngine; using System.Linq; using System.Collections.Generic; public class LinqExample_Join : MonoBehaviour { void Start() { List<Character> characters = new List<Character>() { new Character { Id = 1, Name = "Alice" }, new Character { Id = 2, Name = "Bob" }, new Character { Id = 3, Name = "Charlie" } }; List<Weapon> weapons = new List<Weapon>() { new Weapon { CharacterId = 1, Name = "Sword" }, new Weapon { CharacterId = 2, Name = "Bow" }, new Weapon { CharacterId = 4, Name = "Staff" } // ID 4 没有对应的角色 }; // 使用 Join 连接两个列表 var characterWeapons = characters.Join(weapons, character => character.Id, // 外键选择器 (Character 的 Id) weapon => weapon.CharacterId, // 内键选择器 (Weapon 的 CharacterId) (character, weapon) => new { CharacterName = character.Name, WeaponName = weapon.Name }); // 结果选择器 Debug.Log("角色和武器组合:"); foreach (var combination in characterWeapons) { Debug.Log($"{combination.CharacterName} - {combination.WeaponName}"); } } public class Character { public int Id { get; set; } public string Name { get; set; } } public class Weapon { public int CharacterId { get; set; } public string Name { get; set; } } }

    代码解释:

    • characters.Join(weapons, ...)characters 列表上调用 Join 方法,将 weapons 列表作为内部集合。

    • character => character.Id 指定外键选择器,即从 characters 列表中选择 Id 属性作为连接键。

    • weapon => weapon.CharacterId 指定内键选择器,即从 weapons 列表中选择 CharacterId 属性作为连接键。

    • (character, weapon) => new { CharacterName = character.Name, WeaponName = weapon.Name } 指定结果选择器,定义连接后的结果形式,这里创建了一个匿名对象,包含角色名称和武器名称。

2.9.4.6 聚合操作符 (Aggregation Operators)

  • Count(): 返回集合中元素的数量。

  • Sum(): 计算集合中数值型元素的总和。

  • Min(): 返回集合中最小的元素。

  • Max(): 返回集合中最大的元素。

  • Average(): 计算集合中数值型元素的平均值。

    代码示例 (Unity): 计算场景中所有游戏对象的总数,以及计算所有光源组件的强度总和。

    using UnityEngine; using System.Linq; public class LinqExample_Aggregation : MonoBehaviour { void Start() { GameObject[] allGameObjects = FindObjectsOfType<GameObject>(); Light[] allLights = FindObjectsOfType<Light>(); // 计算游戏对象总数 int gameObjectCount = allGameObjects.Count(); Debug.Log($"游戏对象总数: {gameObjectCount}"); // 计算光源组件强度总和 float totalLightIntensity = allLights.Sum(light => light.intensity); Debug.Log($"光源组件强度总和: {totalLightIntensity}"); // 查找强度最小的光源 Light minIntensityLight = allLights.OrderBy(light => light.intensity).FirstOrDefault(); if (minIntensityLight != null) { Debug.Log($"强度最小的光源: {minIntensityLight.gameObject.name} - Intensity: {minIntensityLight.intensity}"); } } }

    代码解释:

    • allGameObjects.Count() 直接返回 allGameObjects 数组的元素数量。

    • allLights.Sum(light => light.intensity) 使用 Sum 操作符,并使用 Lambda 表达式 light => light.intensity 指定要计算总和的属性为 Light 组件的 intensity 属性。

2.9.4.7 量词操作符 (Quantifier Operators)

  • Any(): 判断集合中是否至少存在一个元素满足指定条件,或集合是否为空。

  • All(): 判断集合中是否所有元素都满足指定条件。

  • Contains(): 判断集合是否包含指定的元素。

    代码示例 (Unity): 检查场景中是否存在标签为 "Player" 的游戏对象,以及检查是否所有游戏对象都带有 Transform 组件。

    using UnityEngine; using System.Linq; public class LinqExample_Quantifier : MonoBehaviour { void Start() { GameObject[] allGameObjects = FindObjectsOfType<GameObject>(); // 检查是否存在标签为 "Player" 的游戏对象 bool hasPlayer = allGameObjects.Any(obj => obj.tag == "Player"); Debug.Log($"场景中是否存在 Player 对象: {hasPlayer}"); // 检查是否所有游戏对象都带有 Transform 组件 (实际上所有 GameObject 都会有 Transform) bool allHaveTransform = allGameObjects.All(obj => obj.GetComponent<Transform>() != null); Debug.Log($"是否所有游戏对象都带有 Transform 组件: {allHaveTransform}"); } }

    代码解释:

    • allGameObjects.Any(obj => obj.tag == "Player") 使用 Any 操作符,并使用 Lambda 表达式 obj => obj.tag == "Player" 作为条件,判断是否存在至少一个游戏对象的标签为 "Player"。

    • allGameObjects.All(obj => obj.GetComponent<Transform>() != null) 使用 All 操作符,并使用 Lambda 表达式 obj => obj.GetComponent<Transform>() != null 作为条件,判断是否所有游戏对象都满足条件(即带有 Transform 组件)。

2.9.4.8 元素操作符 (Element Operators)

  • First() / FirstOrDefault(): 返回集合中的第一个元素。First() 在集合为空时抛出异常,FirstOrDefault() 在集合为空时返回默认值 (例如 null0)。

  • Last() / LastOrDefault(): 返回集合中的最后一个元素。行为与 First() / FirstOrDefault() 类似。

  • Single() / SingleOrDefault(): 返回集合中唯一的元素。Single() 在集合为空或包含多个元素时抛出异常,SingleOrDefault() 在集合为空时返回默认值,在包含多个元素时抛出异常。

  • ElementAt() / ElementAtOrDefault(): 返回集合中指定索引位置的元素。ElementAt() 在索引超出范围时抛出异常,ElementAtOrDefault() 在索引超出范围时返回默认值。

    代码示例 (Unity): 获取场景中的第一个游戏对象,以及获取指定索引位置的游戏对象。

    using UnityEngine; using System.Linq; public class LinqExample_Element : MonoBehaviour { void Start() { GameObject[] allGameObjects = FindObjectsOfType<GameObject>(); if (allGameObjects.Length > 0) { // 获取第一个游戏对象 GameObject firstGameObject = allGameObjects.First(); Debug.Log($"第一个游戏对象: {firstGameObject.name}"); // 获取索引为 2 的游戏对象 (假设存在) if (allGameObjects.Length > 2) { GameObject thirdGameObject = allGameObjects.ElementAtOrDefault(2); // 使用 ElementAtOrDefault 防止索引越界 if (thirdGameObject != null) { Debug.Log($"索引为 2 的游戏对象: {thirdGameObject.name}"); } } } } }

    代码解释:

    • allGameObjects.First() 返回 allGameObjects 数组的第一个元素。

    • allGameObjects.ElementAtOrDefault(2) 返回 allGameObjects 数组索引为 2 的元素,如果索引越界则返回 null。 使用 ElementAtOrDefault 可以避免索引越界异常。

2.9.4.9 转换操作符 (Conversion Operators)

  • ToList():IEnumerable<T> 转换为 List<T>.

  • ToArray():IEnumerable<T> 转换为 T[] (数组).

  • ToDictionary<TKey, TValue>():IEnumerable<T> 转换为 Dictionary<TKey, TValue>.

    代码示例 (Unity): 将筛选后的游戏对象列表转换为 List<GameObject>Dictionary<string, GameObject>

    using UnityEngine; using System.Linq; using System.Collections.Generic; public class LinqExample_Conversion : MonoBehaviour { void Start() { GameObject[] allGameObjects = FindObjectsOfType<GameObject>(); // 筛选标签为 "Enemy" 的游戏对象 var enemyObjects = allGameObjects.Where(obj => obj.tag == "Enemy"); // 转换为 List<GameObject> List<GameObject> enemyList = enemyObjects.ToList(); Debug.Log($"Enemy 对象 List 数量: {enemyList.Count}"); // 转换为 Dictionary<string, GameObject>,以游戏对象名称为键 Dictionary<string, GameObject> enemyDictionary = enemyObjects.ToDictionary(obj => obj.name); Debug.Log($"Enemy 对象 Dictionary 数量: {enemyDictionary.Count}"); if (enemyDictionary.ContainsKey("Enemy_1")) // 假设存在名为 "Enemy_1" 的 Enemy 对象 { Debug.Log($"Dictionary 中找到 Enemy_1 对象: {enemyDictionary["Enemy_1"].name}"); } } }

    代码解释:

    • enemyObjects.ToList() 将 LINQ 查询结果 enemyObjects 转换为 List<GameObject>.

    • enemyObjects.ToDictionary(obj => obj.name) 将 LINQ 查询结果转换为 Dictionary<string, GameObject>,使用 Lambda 表达式 obj => obj.name 指定游戏对象名称作为字典的键。

2.9.5 LINQ 查询流程可视化 (Mermaid Graph TD)

以下 Mermaid 图表展示了 LINQ 查询的基本流程:

图表解释:

  1. 数据源 (Data Source): LINQ 查询的起点,可以是任何实现了 IEnumerable<T>IQueryable<T> 接口的数据源,例如数组、列表、数据库等。

  2. LINQ 查询 (LINQ Query): 使用查询语法或方法语法编写 LINQ 查询语句。

  3. 查询操作符 (Query Operators): LINQ 提供了丰富的查询操作符,例如 Where, Select, OrderBy, GroupBy 等,用于定义查询逻辑。

  4. 延迟执行 (Deferred Execution): LINQ 查询通常是延迟执行的,查询语句本身并不会立即执行,而是会构建一个查询表达式树。

  5. 迭代/具体化操作 (Iteration/Materialization): 只有当需要访问查询结果时(例如使用 foreach 循环迭代,或调用 ToList(), ToArray() 等方法),LINQ 查询才会真正执行。

  6. 结果 (Result): 查询执行后返回的结果集合,类型取决于查询操作符和数据源。

2.9.6 LINQ 的延迟执行特性

LINQ 的延迟执行 (Deferred Execution) 是一个重要的特性,它意味着 LINQ 查询不会在定义时立即执行,而是在真正需要结果时才执行。这种特性带来了以下好处:

  • 性能优化: 只有在需要结果时才执行查询,避免不必要的计算。

  • 链式查询: 可以构建复杂的查询链,只有在最后一步才执行整个查询链,提高效率。

  • 动态查询: 可以在运行时动态修改查询条件,而无需重新执行整个查询。

代码示例 (演示延迟执行):

using UnityEngine; using System.Linq; using System.Collections.Generic; public class LinqExample_DeferredExecution : MonoBehaviour { void Start() { List<int> numbers = new List<int>() { 1, 2, 3, 4, 5 }; // 定义 LINQ 查询,筛选偶数 var evenNumbersQuery = numbers.Where(n => n % 2 == 0); Debug.Log("查询定义完成,但尚未执行"); // 修改原始数据源 numbers.Add(6); numbers.Add(8); Debug.Log("原始数据源已修改"); // 迭代查询结果,此时查询才真正执行 Debug.Log("迭代查询结果,执行查询:"); foreach (var evenNumber in evenNumbersQuery) { Debug.Log(evenNumber); // 输出 2, 4, 6, 8 (包含了修改后的数据) } } }

代码解释:

  • evenNumbersQuery = numbers.Where(n => n % 2 == 0); 定义了一个 LINQ 查询,但此时查询并没有立即执行。

  • numbers.Add(6); numbers.Add(8); 修改了原始数据源 numbers

  • foreach (var evenNumber in evenNumbersQuery) 开始迭代查询结果时,LINQ 查询才真正执行。由于原始数据源已经被修改,因此查询结果包含了修改后的数据 (6 和 8)。

2.9.7 LINQ 在 Unity3D 中的应用场景

LINQ 在 Unity3D 游戏开发中有着广泛的应用场景,以下是一些常见的例子:

  • 游戏对象管理:

    • 查找特定标签、名称、组件的游戏对象。

    • 筛选满足特定条件的游戏对象列表。

    • 对游戏对象列表进行排序、分组。

  • 组件管理:

    • 获取游戏对象上的特定类型组件。

    • 筛选满足特定属性条件的组件列表。

    • 对组件列表进行排序、分组。

  • 数据处理:

    • 处理游戏配置数据、角色属性数据等。

    • 对数据进行筛选、排序、聚合、转换。

  • UI 元素管理:

    • 查找特定类型的 UI 元素。

    • 筛选满足特定条件的 UI 元素列表。

    • 对 UI 元素列表进行排序、分组。

  • 事件处理:

    • 筛选和处理特定类型的事件。

    • 对事件列表进行排序、分组。

2.9.8 LINQ 的性能考量 (Unity3D)

虽然 LINQ 提供了强大的数据处理能力和代码简洁性,但在 Unity3D 游戏开发中,性能始终是一个重要的考量因素。需要注意以下几点:

  • GC (垃圾回收) 开销: LINQ 查询可能会产生临时的匿名对象和集合,导致 GC 开销增加。尤其是在 Update 循环等频繁调用的代码中,应谨慎使用 LINQ。

  • 装箱拆箱: 对值类型数据进行 LINQ 查询时,可能会发生装箱拆箱操作,影响性能。

  • 复杂查询的性能: 对于复杂、大数据量的查询,LINQ 的性能可能不如手写的循环代码。


发布者: 作者: 转发
评论区 (0)
U