解析“60k”大佬的19道C#面试题(上)
<h1 id="解析60k大佬的19道c面试题上">解析“60k”大佬的19道C#面试题(上)</h1><h2 id="先略看题目">先略看题目:</h2>
<ol>
<li>请简述<code>async</code>函数的编译方式</li>
<li>请简述<code>Task</code>状态机的实现和工作机制</li>
<li>请简述<code>await</code>的作用和原理,并说明和<code>GetResult()</code>有什么区别</li>
<li><code>Task</code>和<code>Thread</code>有区别吗?如果有请简述区别</li>
<li>简述<code>yield</code>的作用</li>
<li>利用<code>IEnumerable<T></code>实现斐波那契数列生成</li>
<li>简述<code>stackless coroutine</code>和<code>stackful coroutine</code>的区别,并指出<code>C#</code>的<code>coroutine</code>是哪一种</li>
<li>请简述<code>SelectMany</code>的作用</li>
<li>请实现一个函数<code>Compose</code>用于将多个函数复合</li>
<li>实现<code>Maybe<T></code> <code>monad</code>,并利用<code>LINQ</code>实现对<code>Nothing</code>(空值)和<code>Just</code>(有值)的求和</li>
<li>简述<code>LINQ</code>的<code>lazy computation</code>机制</li>
<li>利用<code>SelectMany</code>实现两个数组中元素的两两相加</li>
<li>请为三元函数实现柯里化</li>
<li>请简述<code>ref struct</code>的作用</li>
<li>请简述<code>ref return</code>的使用方法</li>
<li>请利用<code>foreach</code>和<code>ref</code>为一个数组中的每个元素加<code>1</code></li>
<li>请简述<code>ref</code>、<code>out</code>和<code>in</code>在用作函数参数修饰符时的区别</li>
<li>请简述非<code>sealed</code>类的<code>IDisposable</code>实现方法</li>
<li><code>delegate</code>和<code>event</code>本质是什么?请简述他们的实现机制</li>
</ol>
<p>没错,这是一位来自【广州.NET技术俱乐部】微信群的偏<code>Programming Languages</code>(<code>编程语言开发科学</code>)的大佬,本文我将<strong>斗胆</strong>回答一下这些题目😂。</p>
<p>由于这些题目(对我来说)比较<strong>难</strong>,因此我这次只<strong>斗胆</strong>回答前<code>10</code>道题,发作上篇,另外一半的题目再等我慢慢查阅资料,另行回答😂。</p>
<h1 id="解析">解析:</h1>
<h2 id="1-请简述async函数的编译方式">1. 请简述<code>async</code>函数的编译方式</h2>
<p><code>async</code>/<code>await</code>是<code>C# 5.0</code>推出的异步代码编程模型,其本质是编译为状态机。<strong>只要</strong>函数前带上<code>async</code>,<strong>就会</strong>将函数转换为状态机。</p>
<h2 id="2-请简述task状态机的实现和工作机制">2. 请简述<code>Task</code>状态机的实现和工作机制</h2>
<p><code>CPS</code>全称是<code>Continuation Passing Style</code>,在<code>.NET</code>中,它会自动编译为:</p>
<ol>
<li>将所有引用的局部变量做成闭包,放到一个隐藏的<code>状态机</code>的类中;</li>
<li>将所有的<code>await</code>展开成一个状态号,有几个<code>await</code>就有几个状态号;</li>
<li>每次执行完一个状态,都重复回调<code>状态机</code>的<code>MoveNext</code>方法,同时指定下一个状态号;</li>
<li><code>MoveNext</code>方法还需处理线程和异常等问题。</li>
</ol>
<h2 id="3-请简述await的作用和原理并说明和getresult有什么区别">3. 请简述<code>await</code>的作用和原理,并说明和<code>GetResult()</code>有什么区别</h2>
<p>从状态机的角度出发,<code>await</code>的本质是调用<code>Task.GetAwaiter()</code>的<code>UnsafeOnCompleted(Action)</code>回调,并指定下一个状态号。</p>
<p>从多线程的角度出发,如果<code>await</code>的<code>Task</code>需要在新的线程上执行,该状态机的<code>MoveNext()</code>方法会<strong>立即返回</strong>,此时,<strong>主线程被释放出来了</strong>,然后在<code>UnsafeOnCompleted</code>回调的<code>action</code>指定的线程上下文中继续<code>MoveNext()</code>和下一个状态的代码。</p>
<p>而相比之下,<code>GetResult()</code>就是在当前线程上立即等待<code>Task</code>的完成,在<code>Task</code>完成前,当前线程<strong>不会释放</strong>。</p>
<blockquote>
<p>注意:<code>Task</code>也可能不一定在新的线程上执行,此时用<code>GetResult()</code>或者<code>await</code>就只有会不会创建状态机的区别了。</p>
</blockquote>
<h2 id="4-task和thread有区别吗如果有请简述区别">4. <code>Task</code>和<code>Thread</code>有区别吗?如果有请简述区别</h2>
<p><code>Task</code>和<code>Thread</code>都能创建用多线程的方式执行代码,但它们有较大的区别。</p>
<p><code>Task</code>较新,发布于<code>.NET 4.5</code>,能结合新的<code>async/await</code>代码模型写代码,它不止能创建新线程,还能使用线程池(默认)、单线程等方式编程,在<code>UI</code>编程领域,<code>Task</code>还能自动返回<code>UI</code>线程上下文,还提供了许多便利<code>API</code>以管理多个<code>Task</code>,用表格总结如下:</p>
<table>
<thead>
<tr>
<th>区别</th>
<th>Task</th>
<th>Thread</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>.NET</code>版本</td>
<td><code>4.5</code></td>
<td><code>1.1</code></td>
</tr>
<tr>
<td><code>async/await</code></td>
<td>支持</td>
<td>不支持</td>
</tr>
<tr>
<td>创建新线程</td>
<td>支持</td>
<td>支持</td>
</tr>
<tr>
<td>线程池/单线程</td>
<td>支持</td>
<td>不支持</td>
</tr>
<tr>
<td>返回主线程</td>
<td>支持</td>
<td>不支持</td>
</tr>
<tr>
<td>管理API</td>
<td>支持</td>
<td>不支持</td>
</tr>
</tbody>
</table>
<p><code>TL;DR</code>就是,用<code>Task</code>就对了。</p>
<h2 id="5-简述yield的作用">5. 简述<code>yield</code>的作用</h2>
<p><code>yield</code>需配合<code>IEnumerable<T></code>一起使用,能在一个函数中支持多次(不是多个)返回,其本质和<code>async/await</code>一样,也是状态机。</p>
<p>如果不使用<code>yield</code>,需实现<code>IEnumerable<T></code>,它只暴露了<code>GetEnumerator<T></code>,这样确保<code>yield</code>是可重入的,比较符合人的习惯。</p>
<blockquote>
<p>注意,其它的语言,如<code>C++</code>/<code>Java</code>/<code>ES6</code>实现的<code>yield</code>,都叫<code>generator</code>(生成器),这相当于<code>.NET</code>中的<code>IEnumerator<T></code>(而不是<code>IEnumerable<T></code>)。这种设计导致<code>yield</code>不可重入,<strong>只要其迭代过一次,就无法重新迭代了</strong>,需要注意。</p>
</blockquote>
<h2 id="6-利用ienumerablet实现斐波那契数列生成">6. 利用<code>IEnumerable<T></code>实现斐波那契数列生成</h2>
<pre><code class="language-csharp">IEnumerable<int> GenerateFibonacci(int n)
{
int current = 1, next = 1;
for (int i = 0; i < n; ++i)
{
yield return current;
next = current + (current = next);
}
}
</code></pre>
<h2 id="7-简述stackless-coroutine和stackful-coroutine的区别并指出c的coroutine是哪一种">7. 简述<code>stackless coroutine</code>和<code>stackful coroutine</code>的区别,并指出<code>C#</code>的<code>coroutine</code>是哪一种</h2>
<p><code>stackless</code>和<code>stackful</code>对应的是协程中栈的内存,<code>stackless</code>表示栈内存位置不固定,而<code>stackful</code>则需要分配一个固定的栈内存。</p>
<p>在<code>继续执行</code>(<code>Continuation</code>/<code>MoveNext()</code>)时,<code>stackless</code>需要编译器生成代码,如闭包,来自定义<code>继续执行</code>逻辑;而<code>stackful</code>则直接从原栈的位置<code>继续执行</code>。</p>
<p>性能方面,<code>stackful</code>的中断返回需要依赖控制<code>CPU</code>的跳转位置来实现,属于骚操作,会略微影响<code>CPU</code>的分支预测,从而影响性能(但影响不算大),这方面<code>stackless</code>无影响。</p>
<p>内存方面,<code>stackful</code>需要分配一个固定大小的栈内存(如<code>4kb</code>),而<code>stackless</code>只需创建带一个状态号变量的状态机,<code>stackful</code>占用的内存更大。</p>
<p>骚操作方面,<code>stackful</code>可以轻松实现完全一致的递归/异常处理等,没有任何影响,但<code>stackless</code>需要编译器作者高超的技艺才能实现(如<code>C#</code>的作者),注意最初的<code>C# 5.0</code>在<code>try-catch</code>块中是不能写<code>await</code>的。</p>
<p>和已有组件结合/框架依赖方面,<code>stackless</code>需要定义一个状态机类型,如<code>Task<T></code>/<code>IEnumerable<T></code>/<code>IAsyncEnumerable<T></code>等,而<code>stackful</code>不需要,因此这方面<code>stackless</code>较麻烦。</p>
<p><code>Go</code>属于<code>stackful</code>,因此每个<code>goroutine</code>需要分配一个固定大小的内存。</p>
<p><code>C#</code>属于<code>stackless</code>,它会创建一个闭包和状态机,需要编译器生成代码来指定<code>继续执行</code>逻辑。</p>
<p>总结如下:</p>
<table>
<thead>
<tr>
<th>功能</th>
<th><code>stackless</code></th>
<th><code>stackful</code></th>
</tr>
</thead>
<tbody>
<tr>
<td>内存位置</td>
<td>不固定</td>
<td>固定</td>
</tr>
<tr>
<td>继续执行</td>
<td>编译器定义</td>
<td>CPU跳转</td>
</tr>
<tr>
<td>性能/速度</td>
<td><strong>快</strong></td>
<td>快,但影响分支预测</td>
</tr>
<tr>
<td>内存占用</td>
<td><strong>低</strong></td>
<td>需要固定大小的栈内存</td>
</tr>
<tr>
<td>编译器难度</td>
<td>难</td>
<td><strong>适中</strong></td>
</tr>
<tr>
<td>组件依赖</td>
<td>不方便</td>
<td><strong>方便</strong></td>
</tr>
<tr>
<td>嵌套</td>
<td>不支持</td>
<td><strong>支持</strong></td>
</tr>
<tr>
<td>举例</td>
<td><code>C#</code>/<code>js</code></td>
<td><code>Go</code>/<code>C++ Boost</code></td>
</tr>
</tbody>
</table>
<h2 id="8-请简述selectmany的作用">8. 请简述<code>SelectMany</code>的作用</h2>
<p>相当于<code>js</code>中数组的<code>flatMap</code>,意思是将序列中的<strong>每一条数据</strong>,转换为<strong>0到多条</strong>数据。</p>
<p><code>SelectMany</code>可以实现过滤/<code>.Where</code>,方法如下:</p>
<pre><code class="language-csharp">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>());
}
</code></pre>
<p><code>SelectMany</code>是<code>LINQ</code>中<code>from</code>关键字的组成部分,这一点将在第<code>10</code>题作演示。</p>
<h2 id="9-请实现一个函数compose用于将多个函数复合">9. 请实现一个函数<code>Compose</code>用于将多个函数复合</h2>
<pre><code class="language-csharp">public static Func<T1, T3> Compose<T1, T2, T3>(this Func<T1, T2> f1, Func<T2, T3> f2)
{
return x => f2(f1(x));
}
</code></pre>
<p>然后使用方式:</p>
<pre><code class="language-csharp">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
</code></pre>
<h2 id="10-实现maybet-monad并利用linq实现对nothing空值和just有值的求和">10. 实现<code>Maybe<T></code> <code>monad</code>,并利用<code>LINQ</code>实现对<code>Nothing</code>(空值)和<code>Just</code>(有值)的求和</h2>
<p>本题比较难懂,经过和大佬确认,本质是要实现如下效果:</p>
<pre><code class="language-csharp">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
}
</code></pre>
<p>按照我猴子进化来的大脑的理解,应该很自然地能写出如下代码:</p>
<pre><code class="language-csharp">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>();
}
}
</code></pre>
<p>这种很自然,通过继承<code>IEnumerable<T></code>来实现<code>LINQ to Objects</code>的基本功能,但却是错误答案。</p>
<p>正确答案:</p>
<pre><code class="language-csharp">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>();
}
}
</code></pre>
<p>注意:<br>
首先这是一个函数式编程的应用场景,它应该使用<code>struct</code>——值类型。</p>
<p>其次,不是所有的<code>LINQ</code>都要走<code>IEnumerable<T></code>,可以用手撸的<code>LINQ</code>表达式——<code>SelectMany</code>来表示。(关于这一点,其实特别重要,我稍后有空会深入聊聊这一点。)</p>
<h1 id="总结">总结</h1>
<p>这些技术平时可能比较冷门,全部能回答正确也并不意味着会有多有用,可能很难有机会用上。</p>
<p>但如果是在开发像<code>ASP.NET Core</code>那样的超高性能网络服务器、中间件,或者<code>Unity 3D</code>那样的高性能游戏引擎、或者做一些高性能实时<code>ETL</code>之类的,就能依靠这些知识,做出比肩甚至超过<code>C</code>/<code>C++</code>的性能,同时还能享受<code>C#</code>/<code>.NET</code>便利性的产品。</p>
<blockquote>
<p>群里有人戏称面试时出这些题的公司,要么是心太大,要么至少得开<code>60k</code>,因此本文取名为<code>60k大佬</code>。</p>
</blockquote>
<p>敬请期待我的下篇😂。</p>
<p>喜欢的朋友请关注我的微信公众号:【DotNet骚操作】</p>
<p><img src="https://img2018.cnblogs.com/blog/233608/201908/233608-20190825165420518-990227633.jpg" alt="DotNet骚操作" loading="lazy"></p><br><br>
来源:https://www.cnblogs.com/sdcb/p/20200325-19-csharp-interview-question-from-60k-boss-1.html
頁:
[1]