解析“60k”大佬的19道C#面试题(上)

(21) 2024-02-26 20:12

Hi,大家好,我是编程小6,很荣幸遇见你,我把这些年在开发过程中遇到的问题或想法写出来,今天说一说解析“60k”大佬的19道C#面试题(上),希望能够帮助你!!!。

先略看题目:

  1. 请简述 async函数的编译方式
  2. 请简述 Task状态机的实现和工作机制
  3. 请简述 await的作用和原理,并说明和 GetResult()有什么区别
  4. Task和 Thread有区别吗?如果有请简述区别
  5. 简述 yield的作用
  6. 利用 IEnumerable<T>实现斐波那契数列生成
  7. 简述 stackless coroutine和 stackful coroutine的区别,并指出 C#的 coroutine是哪一种
  8. 请简述 SelectMany的作用
  9. 请实现一个函数 Compose用于将多个函数复合
  10. 实现 Maybe<T> monad,并利用 LINQ实现对 Nothing(空值)和 Just(有值)的求和
  11. 简述 LINQ的 lazy computation机制
  12. 利用 SelectMany实现两个数组中元素的两两相加
  13. 请为三元函数实现柯里化
  14. 请简述 refstruct的作用
  15. 请简述 refreturn的使用方法
  16. 请利用 foreach和 ref为一个数组中的每个元素加 1
  17. 请简述 ref、 out和 in在用作函数参数修饰符时的区别
  18. 请简述非 sealed类的 IDisposable实现方法
  19. delegate和 event本质是什么?请简述他们的实现机制

没错,这是一位来自【广州.NET技术俱乐部】微信群的偏 ProgrammingLanguages( 编程语言开发科学)的大佬,本文我将斗胆回答一下这些题目������。

由于这些题目(对我来说)比较,因此我这次只斗胆回答前 10道题,发作上篇,另外一半的题目再等我慢慢查阅资料,另行回答������。

解析:

1. 请简述 async函数的编译方式

async/ await是 C# 5.0推出的异步代码编程模型,其本质是编译为状态机。只要函数前带上 async,就会将函数转换为状态机。

2. 请简述 Task状态机的实现和工作机制

CPS全称是 ContinuationPassingStyle,在 .NET中,它会自动编译为:

  1. 将所有引用的局部变量做成闭包,放到一个隐藏的 状态机的类中;
  2. 将所有的 await展开成一个状态号,有几个 await就有几个状态号;
  3. 每次执行完一个状态,都重复回调 状态机的 MoveNext方法,同时指定下一个状态号;
  4. MoveNext方法还需处理线程和异常等问题。

3. 请简述 await的作用和原理,并说明和 GetResult()有什么区别

从状态机的角度出发, await的本质是调用 Task.GetAwaiter()的 UnsafeOnCompleted(Action)回调,并指定下一个状态号。

从多线程的角度出发,如果 await的 Task需要在新的线程上执行,该状态机的 MoveNext()方法会立即返回,此时,主线程被释放出来了,然后在 UnsafeOnCompleted回调的 action指定的线程上下文中继续 MoveNext()和下一个状态的代码。

而相比之下, GetResult()就是在当前线程上立即等待 Task的完成,在 Task完成前,当前线程不会释放

注意: Task也可能不一定在新的线程上执行,此时用 GetResult()或者 await就只有会不会创建状态机的区别了。

4. Task和 Thread有区别吗?如果有请简述区别

Task和 Thread都能创建用多线程的方式执行代码,但它们有较大的区别。

Task较新,发布于 .NET4.5,能结合新的 async/await代码模型写代码,它不止能创建新线程,还能使用线程池(默认)、单线程等方式编程,在 UI编程领域, Task还能自动返回 UI线程上下文,还提供了许多便利 API以管理多个 Task,用表格总结如下:

区别TaskThread.NET版本4.51.1async/await支持不支持创建新线程支持支持线程池/单线程支持不支持返回主线程支持不支持管理API支持不支持

TL;DR就是,用 Task就对了。

5. 简述 yield的作用

yield需配合 IEnumerable<T>一起使用,能在一个函数中支持多次(不是多个)返回,其本质和 async/await一样,也是状态机。

如果不使用 yield,需实现 IEnumerable<T>,它只暴露了 GetEnumerator<T>,这样确保 yield是可重入的,比较符合人的习惯。

注意,其它的语言,如 C++/ Java/ ES6实现的 yield,都叫 generator(生成器),这相当于 .NET中的 IEnumerator<T>(而不是 IEnumerable<T>)。这种设计导致 yield不可重入,只要其迭代过一次,就无法重新迭代了,需要注意。

6. 利用 IEnumerable<T>实现斐波那契数列生成

  1. IEnumerable
  2. <int>
  3. GenerateFibonacci
  4. (
  5. int
  6. n
  7. )
  8. {
  9. if
  10. (
  11. n
  12. >=
  13. 1
  14. )
  15. yield
  16. return
  17. 1
  18. ;
  19. int
  20. a
  21. =
  22. 1
  23. ,
  24. b
  25. =
  26. 0
  27. ;
  28. for
  29. (
  30. int
  31. i
  32. =
  33. 2
  34. ;
  35. i
  36. <=
  37. n
  38. ;
  39. ++
  40. i
  41. )
  42. {
  43. int
  44. t
  45. =
  46. b
  47. ;
  48. b
  49. =
  50. a
  51. ;
  52. a
  53. +=
  54. t
  55. ;
  56. yield
  57. return
  58. a
  59. ;
  60. }
  61. }

7. 简述 stackless coroutine和 stackful coroutine的区别,并指出 C#的 coroutine是哪一种

stackless和 stackful对应的是协程中栈的内存, stackless表示栈内存位置不固定,而 stackful则需要分配一个固定的栈内存。

在 继续执行( Continuation/ MoveNext())时, stackless需要编译器生成代码,如闭包,来自定义 继续执行逻辑;而 stackful则直接从原栈的位置 继续执行。

性能方面, stackful的中断返回需要依赖控制 CPU的跳转位置来实现,属于骚操作,会略微影响 CPU的分支预测,从而影响性能(但影响不算大),这方面 stackless无影响。

内存方面, stackful需要分配一个固定大小的栈内存(如 4kb),而 stackless只需创建带一个状态号变量的状态机, stackful占用的内存更大。

骚操作方面, stackful可以轻松实现完全一致的递归/异常处理等,没有任何影响,但 stackless需要编译器作者高超的技艺才能实现(如 C#的作者),注意最初的 C# 5.0在 try-catch块中是不能写 await的。

和已有组件结合/框架依赖方面, stackless需要定义一个状态机类型,如 Task<T>/ IEnumerable<T>/ IAsyncEnumerable<T>等,而 stackful不需要,因此这方面 stackless较麻烦。

Go属于 stackful,因此每个 goroutine需要分配一个固定大小的内存。

C#属于 stackless,它会创建一个闭包和状态机,需要编译器生成代码来指定 继续执行逻辑。

总结如下:

功能stacklessstackful内存位置不固定固定继续执行编译器定义CPU跳转性能/速度快,但影响分支预测内存占用需要固定大小的栈内存编译器难度难适中组件依赖不方便方便嵌套不支持支持举例C#/ jsGo/ C++Boost

8. 请简述 SelectMany的作用

相当于 js中数组的 flatMap,意思是将序列中的每一条数据,转换为0到多条数据。

SelectMany可以实现过滤/ .Where,方法如下:

  1. public
  2. static
  3. IEnumerable
  4. <
  5. T
  6. >
  7. MyWhere
  8. <
  9. T
  10. >(
  11. this
  12. IEnumerable
  13. <
  14. T
  15. >
  16. seq
  17. ,
  18. Func
  19. <
  20. T
  21. ,
  22. bool
  23. >
  24. predicate
  25. )
  26. {
  27. return
  28. seq
  29. .
  30. SelectMany
  31. (
  32. x
  33. =>
  34. predicate
  35. (
  36. x
  37. )
  38. ?
  39. new
  40. []
  41. {
  42. x
  43. }
  44. :
  45. Enumerable
  46. .
  47. Empty
  48. <
  49. T
  50. >());
  51. }

SelectMany是 LINQ中 from关键字的组成部分,这一点将在第 10题作演示。

9. 请实现一个函数 Compose用于将多个函数复合

  1. public
  2. static
  3. Func
  4. <
  5. T1
  6. ,
  7. T3
  8. >
  9. Compose
  10. <
  11. T1
  12. ,
  13. T2
  14. ,
  15. T3
  16. >(
  17. this
  18. Func
  19. <
  20. T1
  21. ,
  22. T2
  23. >
  24. f1
  25. ,
  26. Func
  27. <
  28. T2
  29. ,
  30. T3
  31. >
  32. f2
  33. )
  34. {
  35. return
  36. x
  37. =>
  38. f2
  39. (
  40. f1
  41. (
  42. x
  43. ));
  44. }

然后使用方式:

  1. Func
  2. <
  3. int
  4. ,
  5. double
  6. >
  7. log2
  8. =
  9. x
  10. =>
  11. Math
  12. .
  13. Log2
  14. (
  15. x
  16. );
  17. Func
  18. <
  19. double
  20. ,
  21. string
  22. >
  23. toString
  24. =
  25. x
  26. =>
  27. x
  28. .
  29. ToString
  30. ();
  31. var
  32. log2ToString
  33. =
  34. log2
  35. .
  36. Compose
  37. (
  38. toString
  39. );
  40. Console
  41. .
  42. WriteLine
  43. (
  44. log2ToString
  45. (
  46. 16
  47. ));
  48. // 4

10. 实现 Maybe<T> monad,并利用 LINQ实现对 Nothing(空值)和 Just(有值)的求和

本题比较难懂,经过和大佬确认,本质是要实现如下效果:

  1. void
  2. Main
  3. ()
  4. {
  5. Maybe
  6. <int>
  7. a
  8. =
  9. Maybe
  10. .
  11. Just
  12. (
  13. 5
  14. );
  15. Maybe
  16. <int>
  17. b
  18. =
  19. Maybe
  20. .
  21. Nothing
  22. <int>
  23. ();
  24. Maybe
  25. <int>
  26. c
  27. =
  28. Maybe
  29. .
  30. Just
  31. (
  32. 10
  33. );
  34. (
  35. from
  36. a0
  37. in
  38. a
  39. from
  40. b0
  41. in
  42. b
  43. select
  44. a0
  45. +
  46. b0
  47. ).
  48. Dump
  49. ();
  50. // Nothing
  51. (
  52. from
  53. a0
  54. in
  55. a
  56. from
  57. c0
  58. in
  59. c
  60. select
  61. a0
  62. +
  63. c0
  64. ).
  65. Dump
  66. ();
  67. // Just 15
  68. }

按照我猴子进化来的大脑的理解,应该很自然地能写出如下代码:

  1. public
  2. class
  3. Maybe
  4. <
  5. T
  6. >
  7. :
  8. IEnumerable
  9. <
  10. T
  11. >
  12. {
  13. public
  14. bool
  15. HasValue
  16. {
  17. get
  18. ;
  19. set
  20. ;
  21. }
  22. public
  23. T
  24. Value
  25. {
  26. get
  27. ;
  28. set
  29. ;}
  30. IEnumerable
  31. <
  32. T
  33. >
  34. ToValue
  35. ()
  36. {
  37. if
  38. (
  39. HasValue
  40. )
  41. yield
  42. return
  43. Value
  44. ;
  45. }
  46. public
  47. IEnumerator
  48. <
  49. T
  50. >
  51. GetEnumerator
  52. ()
  53. {
  54. return
  55. ToValue
  56. ().
  57. GetEnumerator
  58. ();
  59. }
  60. IEnumerator
  61. IEnumerable
  62. .
  63. GetEnumerator
  64. ()
  65. {
  66. return
  67. ToValue
  68. ().
  69. GetEnumerator
  70. ();
  71. }
  72. }
  73. public
  74. class
  75. Maybe
  76. {
  77. public
  78. static
  79. Maybe
  80. <
  81. T
  82. >
  83. Just
  84. <
  85. T
  86. >(
  87. T value
  88. )
  89. {
  90. return
  91. new
  92. Maybe
  93. <
  94. T
  95. >
  96. {
  97. Value
  98. =
  99. value
  100. ,
  101. HasValue
  102. =
  103. true
  104. };
  105. }
  106. public
  107. static
  108. Maybe
  109. <
  110. T
  111. >
  112. Nothing
  113. <
  114. T
  115. >()
  116. {
  117. return
  118. new
  119. Maybe
  120. <
  121. T
  122. >();
  123. }
  124. }

这种很自然,通过继承 IEnumerable<T>来实现 LINQ toObjects的基本功能,但却是错误答案。

正确答案:

  1. public
  2. struct
  3. Maybe
  4. <
  5. T
  6. >
  7. {
  8. public
  9. readonly
  10. bool
  11. HasValue
  12. ;
  13. public
  14. readonly
  15. T
  16. Value
  17. ;
  18. public
  19. Maybe
  20. (
  21. bool
  22. hasValue
  23. ,
  24. T value
  25. )
  26. {
  27. HasValue
  28. =
  29. hasValue
  30. ;
  31. Value
  32. =
  33. value
  34. ;
  35. }
  36. public
  37. Maybe
  38. <
  39. B
  40. >
  41. SelectMany
  42. <
  43. TCollection
  44. ,
  45. B
  46. >(
  47. Func
  48. <
  49. T
  50. ,
  51. Maybe
  52. <
  53. TCollection
  54. >>
  55. collectionSelector
  56. ,
  57. Func
  58. <
  59. T
  60. ,
  61. TCollection
  62. ,
  63. B
  64. >
  65. f
  66. )
  67. {
  68. if
  69. (!
  70. HasValue
  71. )
  72. return
  73. Maybe
  74. .
  75. Nothing
  76. <
  77. B
  78. >();
  79. Maybe
  80. <
  81. TCollection
  82. >
  83. collection
  84. =
  85. collectionSelector
  86. (
  87. Value
  88. );
  89. if
  90. (!
  91. collection
  92. .
  93. HasValue
  94. )
  95. return
  96. Maybe
  97. .
  98. Nothing
  99. <
  100. B
  101. >();
  102. return
  103. Maybe
  104. .
  105. Just
  106. (
  107. f
  108. (
  109. Value
  110. ,
  111. collection
  112. .
  113. Value
  114. ));
  115. }
  116. public
  117. override
  118. string
  119. ToString
  120. ()
  121. =>
  122. HasValue
  123. ?
  124. $
  125. "Just {Value}"
  126. :
  127. "Nothing"
  128. ;
  129. }
  130. public
  131. class
  132. Maybe
  133. {
  134. public
  135. static
  136. Maybe
  137. <
  138. T
  139. >
  140. Just
  141. <
  142. T
  143. >(
  144. T value
  145. )
  146. {
  147. return
  148. new
  149. Maybe
  150. <
  151. T
  152. >(
  153. true
  154. ,
  155. value
  156. );
  157. }
  158. public
  159. static
  160. Maybe
  161. <
  162. T
  163. >
  164. Nothing
  165. <
  166. T
  167. >()
  168. {
  169. return
  170. new
  171. Maybe
  172. <
  173. T
  174. >();
  175. }
  176. }

注意:首先这是一个函数式编程的应用场景,它应该使用 struct——值类型。

其次,不是所有的 LINQ都要走 IEnumerable<T>,可以用手撸的 LINQ表达式—— SelectMany来表示。(关于这一点,其实特别重要,我稍后有空会深入聊聊这一点。)

总结

这些技术平时可能比较冷门,全部能回答正确也并不意味着会有多有用,可能很难有机会用上。

但如果是在开发像 ASP.NETCore那样的超高性能网络服务器、中间件,或者 Unity3D那样的高性能游戏引擎、或者做一些高性能实时 ETL之类的,就能依靠这些知识,做出比肩甚至超过 C/ C++的性能,同时还能享受 C#/ .NET便利性的产品。

今天的分享到此就结束了,感谢您的阅读,如果确实帮到您,您可以动动手指转发给其他人。

上一篇

已是最后文章

下一篇

已是最新文章

发表回复