硬核解析十九道C#面試題,看看你能答出來多少?

析“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函數的編譯方式

asyncawait是 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.1
async/await支持不支持
創建新線程支持支持
線程池/單線程支持不支持
返回主線程支持不支持
管理API支持不支持

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

5. 簡述 yield的作用

yield需配合 IEnumerable<T>一起使用,能在一個函數中支持多次(不是多個)返回,其本質和 async/await一樣,也是狀態機。

如果不使用 yield,需實現 IEnumerable<T>,它只暴露了 GetEnumerator<T>,這樣確保 yield是可重入的,比較符合人的習慣。

注意,其它的語言,如 C++JavaES6實現的 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則需要分配一個固定的棧內存。

在 繼續執行( ContinuationMoveNext())時, 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#jsGoC++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之類的,就能依靠這些知識,做出比肩甚至超過 CC++的性能,同時還能享受 C#.NET便利性的產品。

羣裏有人戲稱面試時出這些題的公司,要麼是心太大,要麼至少得開 60k,因此本文取名爲 60k大佬

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章