析“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大佬
。