解析“60k”大佬的19道C#面试题(上)
先略看题目:
请简述
async
函数的编译方式请简述
Task
状态机的实现和工作机制请简述
await
的作用和原理,并说明和GetResult()
有什么区别Task
和Thread
有区别吗?如果有请简述区别简述
yield
的作用利用
IEnumerable<T>
实现斐波那契数列生成简述
stackless coroutine
和stackful coroutine
的区别,并指出C#
的coroutine
是哪一种请简述
SelectMany
的作用请实现一个函数
Compose
用于将多个函数复合实现
Maybe<T>
monad
,并利用LINQ
实现对Nothing
(空值)和Just
(有值)的求和简述
LINQ
的lazy computation
机制利用
SelectMany
实现两个数组中元素的两两相加请为三元函数实现柯里化
请简述
refstruct
的作用请简述
refreturn
的使用方法请利用
foreach
和ref
为一个数组中的每个元素加1
请简述
ref
、out
和in
在用作函数参数修饰符时的区别请简述非
sealed
类的IDisposable
实现方法delegate
和event
本质是什么?请简述他们的实现机制
没错,这是一位来自【广州.NET技术俱乐部】微信群的偏 ProgrammingLanguages
( 编程语言开发科学
)的大佬,本文我将斗胆回答一下这些题目????。
由于这些题目(对我来说)比较难,因此我这次只斗胆回答前 10
道题,发作上篇,另外一半的题目再等我慢慢查阅资料,另行回答????。
解析:
1. 请简述 async
函数的编译方式
async
/ await
是 C# 5.0
推出的异步代码编程模型,其本质是编译为状态机。只要函数前带上 async
,就会将函数转换为状态机。
2. 请简述 Task
状态机的实现和工作机制
CPS
全称是 ContinuationPassingStyle
,在 .NET
中,它会自动编译为:
将所有引用的局部变量做成闭包,放到一个隐藏的
状态机
的类中;将所有的
await
展开成一个状态号,有几个await
就有几个状态号;每次执行完一个状态,都重复回调
状态机
的MoveNext
方法,同时指定下一个状态号;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
,用表格总结如下:
区别 | Task | Thread |
---|---|---|
.NET 版本 | 4.5 | 1.1 |
async/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>
实现斐波那契数列生成
IEnumerable<int> GenerateFibonacci(int n)
{if (n >= 1) yield return 1;int a = 1, b = 0;for (int i = 2; i <= n; ++i){int t = b;b = a;a += t;yield return a;}
}
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
,它会创建一个闭包和状态机,需要编译器生成代码来指定 继续执行
逻辑。
总结如下:
功能 | stackless | stackful |
---|---|---|
内存位置 | 不固定 | 固定 |
继续执行 | 编译器定义 | CPU跳转 |
性能/速度 | 快 | 快,但影响分支预测 |
内存占用 | 低 | 需要固定大小的栈内存 |
编译器难度 | 难 | 适中 |
组件依赖 | 不方便 | 方便 |
嵌套 | 不支持 | 支持 |
举例 | C# / js | Go / C++Boost |
8. 请简述 SelectMany
的作用
相当于 js
中数组的 flatMap
,意思是将序列中的每一条数据,转换为0到多条数据。
SelectMany
可以实现过滤/ .Where
,方法如下:
public static IEnumerable<T> MyWhere<T>(this IEnumerable<T> seq, Func<T, bool> predicate)
{return seq.SelectMany(x => predicate(x) ? new[] { x } : Enumerable.Empty<T>());
}
SelectMany
是 LINQ
中 from
关键字的组成部分,这一点将在第 10
题作演示。
9. 请实现一个函数 Compose
用于将多个函数复合
public static Func<T1, T3> Compose<T1, T2, T3>(this Func<T1, T2> f1, Func<T2, T3> f2)
{return x => f2(f1(x));
}
然后使用方式:
Func<int, double> log2 = x => Math.Log2(x);
Func<double, string> toString = x => x.ToString();
var log2ToString = log2.Compose(toString);
Console.WriteLine(log2ToString(16)); // 4
10. 实现 Maybe<T>
monad
,并利用 LINQ
实现对 Nothing
(空值)和 Just
(有值)的求和
本题比较难懂,经过和大佬确认,本质是要实现如下效果:
void Main()
{Maybe<int> a = Maybe.Just(5);Maybe<int> b = Maybe.Nothing<int>();Maybe<int> c = Maybe.Just(10);(from a0 in a from b0 in b select a0 + b0).Dump(); // Nothing(from a0 in a from c0 in c select a0 + c0).Dump(); // Just 15
}
按照我猴子进化来的大脑的理解,应该很自然地能写出如下代码:
public class Maybe<T> : IEnumerable<T>
{public bool HasValue { get; set; }public T Value { get; set;}IEnumerable<T> ToValue(){if (HasValue) yield return Value;}public IEnumerator<T> GetEnumerator(){return ToValue().GetEnumerator();}IEnumerator IEnumerable.GetEnumerator(){return ToValue().GetEnumerator();}
}
public class Maybe
{public static Maybe<T> Just<T>(T value){return new Maybe<T> { Value = value, HasValue = true};}public static Maybe<T> Nothing<T>(){return new Maybe<T>();}
}
这种很自然,通过继承 IEnumerable<T>
来实现 LINQ toObjects
的基本功能,但却是错误答案。
正确答案:
public struct Maybe<T>
{public readonly bool HasValue;public readonly T Value;public Maybe(bool hasValue, T value){HasValue = hasValue;Value = value;}public Maybe<B> SelectMany<TCollection, B>(Func<T, Maybe<TCollection>> collectionSelector, Func<T, TCollection, B> f){if (!HasValue) return Maybe.Nothing<B>();Maybe<TCollection> collection = collectionSelector(Value);if (!collection.HasValue) return Maybe.Nothing<B>();return Maybe.Just(f(Value, collection.Value));}public override string ToString() => HasValue ? $"Just {Value}" : "Nothing";
}
public class Maybe
{public static Maybe<T> Just<T>(T value){return new Maybe<T>(true, value);}public static Maybe<T> Nothing<T>(){return new Maybe<T>();}
}
注意:首先这是一个函数式编程的应用场景,它应该使用 struct
——值类型。
其次,不是所有的 LINQ
都要走 IEnumerable<T>
,可以用手撸的 LINQ
表达式—— SelectMany
来表示。(关于这一点,其实特别重要,我稍后有空会深入聊聊这一点。)
总结
这些技术平时可能比较冷门,全部能回答正确也并不意味着会有多有用,可能很难有机会用上。
但如果是在开发像 ASP.NETCore
那样的超高性能网络服务器、中间件,或者 Unity3D
那样的高性能游戏引擎、或者做一些高性能实时 ETL
之类的,就能依靠这些知识,做出比肩甚至超过 C
/ C++
的性能,同时还能享受 C#
/ .NET
便利性的产品。
群里有人戏称面试时出这些题的公司,要么是心太大,要么至少得开
60k
,因此本文取名为60k大佬
。
敬请期待我的下篇????。