试试 IEnumerable 的另外 6 个小例子
IEnumerable 接口是 C# 开发过程中非常重要的接口,对于其特性和用法的了解是十分必要的。本文将通过6个小例子,来熟悉一下其简单的用法。
<!– more –>
阅读建议
- 在阅读本篇时,建议先阅读前篇《试试IEnumerable的10个小例子》,更加助于读者理解。
- 阅读并理解本篇需要花费5-10分钟左右的时间,而且其中包含一些实践建议。建议先收藏本文,闲时阅读并实践。
全是源码
以下便是这6个小例子,相应的说明均标记在注释中。
每个以 TXX 开头命名的均是一个示例。建议从上往下阅读。
- 1 using System;
- 2 using System.Collections.Generic;
- 3 using System.Linq;
- 4 using FluentAssertions;
- 5 using Xunit;
- 6 using Xunit.Abstractions;
- 7
- 8 namespace Try_More_On_IEnumerable
- 9 {
- 10 public class EnumerableTests2
- 11 {
- 12 private readonly ITestOutputHelper _testOutputHelper;
- 13
- 14 public EnumerableTests2(
- 15 ITestOutputHelper testOutputHelper)
- 16 {
- 17 _testOutputHelper = testOutputHelper;
- 18 }
- 19
- 20 [Fact]
- 21 public void T11分组合并()
- 22 {
- 23 var array1 = new[] {0, 1, 2, 3, 4};
- 24 var array2 = new[] {5, 6, 7, 8, 9};
- 25
- 26 // 通过本地方法合并两个数组为一个数据
- 27 var result1 = ConcatArray(array1, array2).ToArray();
- 28
- 29 // 使用 Linq 中的 Concat 来合并两个 IEnumerable 对象
- 30 var result2 = array1.Concat(array2).ToArray();
- 31
- 32 // 使用 Linq 中的 SelectMany 将 “二维数据” 拉平合并为一个数组
- 33 var result3 = new[] {array1, array2}.SelectMany(x => x).ToArray();
- 34
- 35 /**
- 36 * 使用 Enumerable.Range 生成一个数组,这个数据的结果为
- 37 * 0,1,2,3,4,5,6,7,8,9
- 38 */
- 39 var result = Enumerable.Range(0, 10).ToArray();
- 40
- 41 // 通过以上三种方式合并的结果时相同的
- 42 result1.Should().Equal(result);
- 43 result2.Should().Equal(result);
- 44 result3.Should().Equal(result);
- 45
- 46 IEnumerable<T> ConcatArray<T>(IEnumerable<T> source1, IEnumerable<T> source2)
- 47 {
- 48 foreach (var item in source1)
- 49 {
- 50 yield return item;
- 51 }
- 52
- 53 foreach (var item in source2)
- 54 {
- 55 yield return item;
- 56 }
- 57 }
- 58 }
- 59
- 60 [Fact]
- 61 public void T12拉平三重循环()
- 62 {
- 63 /**
- 64 * 通过本地函数获取 0-999 共 1000 个数字。
- 65 * 在 GetSomeData 通过三重循环构造这些数据
- 66 * 值得注意的是 GetSomeData 隐藏了三重循环的细节
- 67 */
- 68 var result1 = GetSomeData(10, 10, 10)
- 69 .ToArray();
- 70
- 71 /**
- 72 * 与 GetSomeData 方法对比,将“遍历”和“处理”两个逻辑进行了分离。
- 73 * “遍历”指的是三重循环本身。
- 74 * “处理”指的是三重循环最内部的加法过程。
- 75 * 这里通过 Select 方法,将“处理”过程抽离了出来。
- 76 * 这其实和 “T03分离条件”中使用 Where 使用的是相同的思想。
- 77 */
- 78 var result2 = GetSomeData2(10, 10, 10)
- 79 .Select(tuple => tuple.i * 100 + tuple.j * 10 + tuple.k)
- 80 .ToArray();
- 81
- 82 // 生成一个 0-999 的数组。
- 83 var result = Enumerable.Range(0, 1000).ToArray();
- 84
- 85 result1.Should().Equal(result);
- 86 result2.Should().Equal(result);
- 87
- 88 IEnumerable<int> GetSomeData(int maxI, int maxJ, int maxK)
- 89 {
- 90 for (var i = 0; i < maxI; i++)
- 91 {
- 92 for (var j = 0; j < maxJ; j++)
- 93 {
- 94 for (var k = 0; k < maxK; k++)
- 95 {
- 96 yield return i * 100 + j * 10 + k;
- 97 }
- 98 }
- 99 }
- 100 }
- 101
- 102 IEnumerable<(int i, int j, int k)> GetSomeData2(int maxI, int maxJ, int maxK)
- 103 {
- 104 for (var i = 0; i < maxI; i++)
- 105 {
- 106 for (var j = 0; j < maxJ; j++)
- 107 {
- 108 for (var k = 0; k < maxK; k++)
- 109 {
- 110 yield return (i, j, k);
- 111 }
- 112 }
- 113 }
- 114 }
- 115 }
- 116
- 117 private class TreeNode
- 118 {
- 119 public TreeNode()
- 120 {
- 121 Children = Enumerable.Empty<TreeNode>();
- 122 }
- 123
- 124 /// <summary>
- 125 /// 当前节点的值
- 126 /// </summary>
- 127 public int Value { get; set; }
- 128
- 129 /// <summary>
- 130 /// 当前节点的子节点列表
- 131 /// </summary>
- 132 public IEnumerable<TreeNode> Children { get; set; }
- 133 }
- 134
- 135 [Fact]
- 136 public void T13遍历树()
- 137 {
- 138 /**
- 139 * 树结构如下:
- 140 * └─0
- 141 * ├─1
- 142 * │ └─3
- 143 * └─2
- 144 */
- 145 var tree = new TreeNode
- 146 {
- 147 Value = 0,
- 148 Children = new[]
- 149 {
- 150 new TreeNode
- 151 {
- 152 Value = 1,
- 153 Children = new[]
- 154 {
- 155 new TreeNode
- 156 {
- 157 Value = 3
- 158 },
- 159 }
- 160 },
- 161 new TreeNode
- 162 {
- 163 Value = 2
- 164 },
- 165 }
- 166 };
- 167
- 168 // 深度优先遍历的结果
- 169 var dftResult = new[] {0, 1, 3, 2};
- 170
- 171 // 通过迭代器实现深度优先遍历
- 172 var dft = DFTByEnumerable(tree).ToArray();
- 173 dft.Should().Equal(dftResult);
- 174
- 175 // 使用堆栈配合循环算法实现深度优先遍历
- 176 var dftList = DFTByStack(tree).ToArray();
- 177 dftList.Should().Equal(dftResult);
- 178
- 179 // 递归算法实现深度优先遍历
- 180 var dftByRecursion = DFTByRecursion(tree).ToArray();
- 181 dftByRecursion.Should().Equal(dftResult);
- 182
- 183 // 广度优先遍历的结果
- 184 var bdfResult = new[] {0, 1, 2, 3};
- 185
- 186 /**
- 187 * 通过迭代器实现广度优先遍历
- 188 * 此处未提供“通过队列配合循环算法”和“递归算法”实现广度优先遍历的两种算法进行对比。读者可以自行尝试。
- 189 */
- 190 var bft = BFT(tree).ToArray();
- 191 bft.Should().Equal(bdfResult);
- 192
- 193 /**
- 194 * 迭代器深度优先遍历
- 195 * depth-first traversal
- 196 */
- 197 IEnumerable<int> DFTByEnumerable(TreeNode root)
- 198 {
- 199 yield return root.Value;
- 200 foreach (var child in root.Children)
- 201 {
- 202 foreach (var item in DFTByEnumerable(child))
- 203 {
- 204 yield return item;
- 205 }
- 206 }
- 207 }
- 208
- 209 // 使用堆栈配合循环算法实现深度优先遍历
- 210 IEnumerable<int> DFTByStack(TreeNode root)
- 211 {
- 212 var result = new List<int>();
- 213 var stack = new Stack<TreeNode>();
- 214 stack.Push(root);
- 215 while (stack.TryPop(out var node))
- 216 {
- 217 result.Add(node.Value);
- 218 foreach (var nodeChild in node.Children.Reverse())
- 219 {
- 220 stack.Push(nodeChild);
- 221 }
- 222 }
- 223
- 224 return result;
- 225 }
- 226
- 227 // 递归算法实现深度优先遍历
- 228 IEnumerable<int> DFTByRecursion(TreeNode root)
- 229 {
- 230 var list = new List<int> {root.Value};
- 231 foreach (var rootChild in root.Children)
- 232 {
- 233 list.AddRange(DFTByRecursion(rootChild));
- 234 }
- 235
- 236 return list;
- 237 }
- 238
- 239 // 通过迭代器实现广度优先遍历
- 240 IEnumerable<int> BFT(TreeNode root)
- 241 {
- 242 yield return root.Value;
- 243
- 244 foreach (var bftChild in BFTChildren(root.Children))
- 245 {
- 246 yield return bftChild;
- 247 }
- 248
- 249 IEnumerable<int> BFTChildren(IEnumerable<TreeNode> children)
- 250 {
- 251 var tempList = new List<TreeNode>();
- 252 foreach (var treeNode in children)
- 253 {
- 254 tempList.Add(treeNode);
- 255 yield return treeNode.Value;
- 256 }
- 257
- 258 foreach (var bftChild in tempList.SelectMany(treeNode => BFTChildren(treeNode.Children)))
- 259 {
- 260 yield return bftChild;
- 261 }
- 262 }
- 263 }
- 264 }
- 265
- 266 [Fact]
- 267 public void T14搜索树()
- 268 {
- 269 /**
- 270 * 此处所指的搜索树是指在遍历树的基础上增加终结遍历的条件。
- 271 * 因为一般构建搜索树是为了找到第一个满足条件的数据,因此与单纯的遍历存在不同。
- 272 * 树结构如下:
- 273 * └─0
- 274 * ├─1
- 275 * │ └─3
- 276 * └─5
- 277 * └─2
- 278 */
- 279
- 280 var tree = new TreeNode
- 281 {
- 282 Value = 0,
- 283 Children = new[]
- 284 {
- 285 new TreeNode
- 286 {
- 287 Value = 1,
- 288 Children = new[]
- 289 {
- 290 new TreeNode
- 291 {
- 292 Value = 3
- 293 },
- 294 }
- 295 },
- 296 new TreeNode
- 297 {
- 298 Value = 5,
- 299 Children = new[]
- 300 {
- 301 new TreeNode
- 302 {
- 303 Value = 2
- 304 },
- 305 }
- 306 },
- 307 }
- 308 };
- 309
- 310 /**
- 311 * 有了深度优先遍历算法的情况下,再增加一个条件判断,便可以实现深度优先的搜索
- 312 * 搜索树中第一个大于等于 3 并且是奇数的数字
- 313 */
- 314 var result = DFS(tree, x => x >= 3 && x % 2 == 1);
- 315
- 316 /**
- 317 * 搜索到的结果是3。
- 318 * 特别提出,如果使用广度优先搜索,结果应该是5。
- 319 * 读者可以通过 T13遍历树 中的广度优先遍历算法配合 FirstOrDefault 中相同的条件实现。
- 320 * 建议读者尝试以上代码尝试一下。
- 321 */
- 322 result.Should().Be(3);
- 323
- 324 int DFS(TreeNode root, Func<int, bool> predicate)
- 325 {
- 326 var re = DFTByEnumerable(root)
- 327 .FirstOrDefault(predicate);
- 328 return re;
- 329 }
- 330
- 331 // 迭代器深度优先遍历
- 332 IEnumerable<int> DFTByEnumerable(TreeNode root)
- 333 {
- 334 yield return root.Value;
- 335 foreach (var child in root.Children)
- 336 {
- 337 foreach (var item in DFTByEnumerable(child))
- 338 {
- 339 yield return item;
- 340 }
- 341 }
- 342 }
- 343 }
- 344
- 345 [Fact]
- 346 public void T15分页()
- 347 {
- 348 var arraySource = new[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
- 349
- 350 // 使用迭代器进行分页,每 3 个一页
- 351 var enumerablePagedResult = PageByEnumerable(arraySource, 3).ToArray();
- 352
- 353 // 结果一共 4 页
- 354 enumerablePagedResult.Should().HaveCount(4);
- 355 // 最后一页只有一个数字,为 9
- 356 enumerablePagedResult.Last().Should().Equal(9);
- 357
- 358
- 359 // 通过常规的 Skip 和 Take 来分页是最为常见的办法。结果应该与上面的分页结果一样
- 360 var result3 = NormalPage(arraySource, 3).ToArray();
- 361
- 362 result3.Should().HaveCount(4);
- 363 result3.Last().Should().Equal(9);
- 364
- 365 IEnumerable<IEnumerable<int>> PageByEnumerable(IEnumerable<int> source, int pageSize)
- 366 {
- 367 var onePage = new LinkedList<int>();
- 368 foreach (var i in source)
- 369 {
- 370 onePage.AddLast(i);
- 371 if (onePage.Count != pageSize)
- 372 {
- 373 continue;
- 374 }
- 375
- 376 yield return onePage;
- 377 onePage = new LinkedList<int>();
- 378 }
- 379
- 380 // 最后一页如果数据不足一页,也应该返回该页
- 381 if (onePage.Count > 0)
- 382 {
- 383 yield return onePage;
- 384 }
- 385 }
- 386
- 387 IEnumerable<IEnumerable<int>> NormalPage(IReadOnlyCollection<int> source, int pageSize)
- 388 {
- 389 var pageCount = Math.Ceiling(1.0 * source.Count / pageSize);
- 390 for (var i = 0; i < pageCount; i++)
- 391 {
- 392 var offset = i * pageSize;
- 393 var onePage = source
- 394 .Skip(offset)
- 395 .Take(pageSize);
- 396 yield return onePage;
- 397 }
- 398 }
- 399
- 400 /**
- 401 * 从写法逻辑上来看,显然 NormalPage 的写法更容易让大众接受
- 402 * PageByEnumerable 写法在仅仅只有在一些特殊的情况下才能体现性能上的优势,可读性上却不如 NormalPage
- 403 */
- 404 }
- 405
- 406 [Fact]
- 407 public void T16分页与多级缓存()
- 408 {
- 409 /**
- 410 * 获取 5 页数据,每页 2 个。
- 411 * 依次从 内存、Redis、ElasticSearch和数据库中获取数据。
- 412 * 先从内存中获取数据,如果内存中数据不足页,则从 Redis 中获取。
- 413 * 若 Redis 获取后还是不足页,进而从 ElasticSearch 中获取。依次类推,直到足页或者再无数据
- 414 */
- 415 const int pageSize = 2;
- 416 const int pageCount = 5;
- 417 var emptyData = Enumerable.Empty<int>().ToArray();
- 418
- 419 /**
- 420 * 初始化各数据源的数据,除了内存有数据外,其他数据源均没有数据
- 421 */
- 422 var memoryData = new[] {0, 1, 2};
- 423 var redisData = emptyData;
- 424 var elasticSearchData = emptyData;
- 425 var databaseData = emptyData;
- 426
- 427 var result = GetSourceData()
- 428 // ToPagination 是一个扩展方法。此处是为了体现链式调用的可读性,转而使用扩展方法,没有使用本地函数
- 429 .ToPagination(pageCount, pageSize)
- 430 .ToArray();
- 431
- 432 result.Should().HaveCount(2);
- 433 result[0].Should().Equal(0, 1);
- 434 result[1].Should().Equal(2);
- 435
- 436 /**
- 437 * 初始化各数据源数据,各个数据源均有一些数据
- 438 */
- 439 memoryData = new[] {0, 1, 2};
- 440 redisData = new[] {3, 4, 5};
- 441 elasticSearchData = new[] {6, 7, 8};
- 442 databaseData = Enumerable.Range(9, 100).ToArray();
- 443
- 444 var result2 = GetSourceData()
- 445 .ToPagination(pageCount, pageSize)
- 446 .ToArray();
- 447
- 448 result2.Should().HaveCount(5);
- 449 result2[0].Should().Equal(0, 1);
- 450 result2[1].Should().Equal(2, 3);
- 451 result2[2].Should().Equal(4, 5);
- 452 result2[3].Should().Equal(6, 7);
- 453 result2[4].Should().Equal(8, 9);
- 454
- 455 IEnumerable<int> GetSourceData()
- 456 {
- 457 // 将多数据源的数据连接在一起
- 458 var data = GetDataSource()
- 459 .SelectMany(x => x);
- 460 return data;
- 461
- 462 // 获取数据源
- 463 IEnumerable<IEnumerable<int>> GetDataSource()
- 464 {
- 465 // 将数据源依次返回
- 466 yield return GetFromMemory();
- 467 yield return GetFromRedis();
- 468 yield return GetFromElasticSearch();
- 469 yield return GetFromDatabase();
- 470 }
- 471
- 472 IEnumerable<int> GetFromMemory()
- 473 {
- 474 _testOutputHelper.WriteLine("正在从内存中获取数据");
- 475 return memoryData;
- 476 }
- 477
- 478 IEnumerable<int> GetFromRedis()
- 479 {
- 480 _testOutputHelper.WriteLine("正在从Redis中获取数据");
- 481 return redisData;
- 482 }
- 483
- 484 IEnumerable<int> GetFromElasticSearch()
- 485 {
- 486 _testOutputHelper.WriteLine("正在从ElasticSearch中获取数据");
- 487 return elasticSearchData;
- 488 }
- 489
- 490 IEnumerable<int> GetFromDatabase()
- 491 {
- 492 _testOutputHelper.WriteLine("正在从数据库中获取数据");
- 493 return databaseData;
- 494 }
- 495 }
- 496
- 497 /**
- 498 * 值得注意的是:
- 499 * 由于 Enumerable 按需迭代的特性,如果将 result2 的所属页数改为只获取 1 页。
- 500 * 则在执行数据获取时,将不会再控制台中输出从 Redis、ElasticSearch和数据库中获取数据。
- 501 * 也就是说,并没有执行这些操作。读者可以自行修改以上代码,加深印象。
- 502 */
- 503 }
- 504 }
- 505
- 506 public static class EnumerableExtensions
- 507 {
- 508 /// <summary>
- 509 /// 将原数据分页
- 510 /// </summary>
- 511 /// <param name="source">数据源</param>
- 512 /// <param name="pageCount">页数</param>
- 513 /// <param name="pageSize">页大小</param>
- 514 /// <returns></returns>
- 515 public static IEnumerable<IEnumerable<int>> ToPagination(this IEnumerable<int> source,
- 516 int pageCount,
- 517 int pageSize)
- 518 {
- 519 var maxCount = pageCount * pageSize;
- 520 var countNow = 0;
- 521 var onePage = new LinkedList<int>();
- 522 foreach (var i in source)
- 523 {
- 524 onePage.AddLast(i);
- 525 countNow++;
- 526
- 527 // 如果获取的数量已经达到了分页所需要的总数,则停止进一步迭代
- 528 if (countNow == maxCount)
- 529 {
- 530 break;
- 531 }
- 532
- 533 if (onePage.Count != pageSize)
- 534 {
- 535 continue;
- 536 }
- 537
- 538 yield return onePage;
- 539 onePage = new LinkedList<int>();
- 540 }
- 541
- 542 // 最后一页如果数据不足一页,也应该返回该页
- 543 if (onePage.Count > 0)
- 544 {
- 545 yield return onePage;
- 546 }
- 547 }
- 548 }
- 549 }
源码说明
以上示例的源代码放置于博客示例代码库中。
项目采用 netcore 2.2 作为目标框架,因此需要安装 netcore 2.2 SDK 才能运行。
- 本文链接: http://www.newbe.pro/2019/09/10/Others/Try-More-On-IEnumerable-2/
- 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!