IEnumerable 接口是 C# 开发过程中非常重要的接口,对于其特性和用法的了解是十分必要的。本文将通过6个小例子,来熟悉一下其简单的用法。

<!– more –>

  • 在阅读本篇时,建议先阅读前篇《试试IEnumerable的10个小例子》,更加助于读者理解。
  • 阅读并理解本篇需要花费5-10分钟左右的时间,而且其中包含一些实践建议。建议先收藏本文,闲时阅读并实践。

以下便是这6个小例子,相应的说明均标记在注释中。

每个以 TXX 开头命名的均是一个示例。建议从上往下阅读。

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

 

  

以上示例的源代码放置于博客示例代码库中。

项目采用 netcore 2.2 作为目标框架,因此需要安装 netcore 2.2 SDK 才能运行。

 

版权声明:本文为newbe36524原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/newbe36524/p/11503811.html