溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

C#中的接口默認方法是什么?

發(fā)布時間:2020-05-23 10:11:25 來源:億速云 閱讀:670 作者:Leah 欄目:編程語言

C#中接口默認方法是什么?相信很多新手小白對C#中接口默認方法的了解處于懵懂狀態(tài),通過這篇文章的總結(jié),希望你能有所收獲。如下資料是關于接口默認方法的內(nèi)容。

interface IStringList {
    void Add(string o); // 添加元素
    void Remove(int i); // 刪除元素
    string Get(int i);  // 獲取元素
    int Length { get; } // 獲取列表長度
}

不管怎么說,這個列表已經(jīng)擁有了基本的增刪除改查功能,比如遍歷,可以這樣寫

IStringList list = createList();
for (var i = 0; i < list.Length; i++) {
    string o = list.Get(i);
    // Do something with o
}

這個 IStringList 作為一個基礎接口在類庫中發(fā)布之后,大量的程序員使用了這個接口,實現(xiàn)了一堆各種各種各樣的列表,像 StringArrayList、LinkedStringListStringQueue、StringStack、SortedStringList……有抽象類,有擴展接口,也有各種實現(xiàn)類??傊?jīng)過較長一段時間的積累,IStringList 的子孫遍布全球。

然后 IStringList 的發(fā)明者,決定為列表定義更多的方法,以適合在技術飛速發(fā)展下開發(fā)者們對 IStringList 使用便捷性的要求,于是

interface IStringList {
    int IndexOf(string o);          // 查找元素的索引,未找到返回 -1
    void Insert(string o, int i);   // 在指定位置插入元素

    // ------------------------------
    void Add(string o); // 添加元素
    void Remove(int i); // 刪除元素
    string Get(int i);  // 獲取元素
    int Length { get; } // 獲取列表長度
}

當然,接口變化之外所有實現(xiàn)類都必須實現(xiàn)它,不然編譯器會報錯,基礎庫的抽象類 AbstractStringList 中實現(xiàn)了上述新增加的接口。整個基礎庫完美編譯,發(fā)布了 2.0 版本。

然而,現(xiàn)實非常殘酷!

基礎庫的用戶們(開發(fā)者)發(fā)出了極大的報怨聲,因為他們太多代碼編譯不過了!

是的,并不是所有用戶都會直接繼承 AbstractStringList,很多用戶直接實現(xiàn)了 IStringList。還有不少用戶甚至擴展了 IStringList,但他們沒有定義 int IndexOf(string o) 而是定義的 int Find(string o)。由于基礎庫接口 IStringList 的變化,用戶們需要花大量地時間去代碼來實現(xiàn) IStringList 中定義的新方法。

這個例子是提到了 IStringList,只添加了兩個方法。這對用戶造成的麻煩雖然已經(jīng)不小,但工作量還算可以接受。但是想想 JDK 和 .NET Framework/Core 龐大的基礎庫,恐怕用戶只能用“崩潰”來形容!

2. 辦法

肯定不能讓用戶崩潰,得想辦法解決這個問題。于是,Java 和 C# 的兩個方案出現(xiàn)了

  • Java 提出了默認方法,即在接口中添加默認實現(xiàn)
  • C# 提出了擴展方法,即通過改變靜態(tài)方法的調(diào)用形式來假裝是對象調(diào)用

不得不說 C# 的擴展方法很聰明,但它畢竟不是真正對接口進行擴展,所以在 C# 8 中也加入了默認方法來解決接口擴展造成的問題。

接口擴展方法提出來之后,雖然解決了默認實現(xiàn)的問題,卻又帶出了新的問題。

  • 接口實現(xiàn)了默認方法,實現(xiàn)接口的類還需要實現(xiàn)嗎?如果不實現(xiàn)會怎么樣?
  • 無論 Java 還是 C# 都不允許類多繼承,但是接口可以。而接口中的默認實現(xiàn)帶來了類似于類多繼承所產(chǎn)生的問題,怎么辦?
  • 在復雜的實現(xiàn)和繼承關系中,最終執(zhí)行的到底會是哪一個方法?

3. 問題一,默認方法和類實現(xiàn)方法的關系

忽略上面 IStringList 接口中補充的 Insert(Object, int) 方法,我們把關注點放在 IndexOf(Object) 上。Java 和 C# 的語法異曲同工:

3.1. 先來看看默認方法的語法

  • Java 版
interface StringList {
    void add(Object s);
    void remove(int i);
    Object get(int i);
    int getLength();

    default int indexOf(Object s) {
        for (int i = 0; i < getLength(); i++) {
            if (get(i) == s) { return i; }
        }
        return -1;
    }
}
  • C# 版
interface IStringList
{
    public void Add(string s);
    void Remove(int i);
    string Get(int i);
    int Length { get; }
    int IndexOf(string s)
    {
        for (var i = 0; i < Length; i++)
        {
            if (Get(i) == s) { return i; }
        }
        return -1;
    }
}

這里把 C# 和 Java 的接口都寫出來,主要是因為二者講法和命名規(guī)范略有不同。接下來進行的研究 C# 和 Java 行為相似的地方,就主要以 C# 為例了。

怎么區(qū)分是 C# 示例還是 Java 示例?看代碼規(guī)范,最明顯的是 C# 方法用 Pascal 命名規(guī)則,Java 方法用 camel 命名規(guī)則。當然,還有 Lambda 的箭頭也不一樣。

接下來的實現(xiàn),僅以 C# 為例:

class MyList : IStringList
{
    List<string> list = new List<string>();  // 偷懶用現(xiàn)成的

    public int Length => list.Count;
    public void Add(string o) => list.Add(o);
    public string Get(int i) => list[i];
    public void Remove(int i) => list.RemoveAt(i);
}

MyList 沒有實現(xiàn) IndexOf,但是使用起來不會有任何問題

class Program
{
    static void Main(string[] args)
    {
        IStringList myList = new MyList();
        myList.Add("First");
        myList.Add("Second");
        myList.Add("Third");

        Console.WriteLine(myList.IndexOf("Third"));  // 輸出 2
        Console.WriteLine(myList.IndexOf("first"));  // 輸出 -1,注意 first 大小寫
    }
}

3.2. 在 MyList 中實現(xiàn) IndexOf

現(xiàn)在,在 MyList 中添加 IndexOf,實現(xiàn)對字符串忽略大小寫的查找:

// 這里用 partial class 表示是部分實現(xiàn),
// 對不住 javaer,Java 沒有部分類語法
partial class MyList
{
    public int IndexOf(string s)
    {
        return list.FindIndex(el =>
        {
            return el == s
                || (el != null && el.Equals(s, StringComparison.OrdinalIgnoreCase));
        });
    }
}

然后 Main 函數(shù)中輸出的內(nèi)容變了

Console.WriteLine(myList.IndexOf("Third")); // 還是返回 2
Console.WriteLine(myList.IndexOf("first")); // 返回 0,不是 -1

顯然這里調(diào)用了 MyList.IndexOf()。

3.3. 結(jié)論,以及 Java 和 C# 的不同之處

上面主要是以 C# 作為示例,其實 Java 也是一樣的。上面的示例中是通過接口類型來調(diào)用的 IndexOf 方法。第一次調(diào)用的是 IStringList.IndexOf 默認實現(xiàn),因為這時候 MyList 并沒有實現(xiàn) IndexOf;第二次調(diào)用的是 MyList.IndexOf 實現(xiàn)。筆者使用 Java 寫了類似的代碼,行為完全一致。

因此,對于默認方法,會優(yōu)先調(diào)用類中的實現(xiàn),如果類中沒有實現(xiàn)具有默認方法的接口,才會去調(diào)用接口中的默認方法。

但是?。。∏懊娴氖纠鞘褂玫慕涌陬愋鸵脤崿F(xiàn),如果換成實例類類型來引用實例呢?

如果 MyList 中實現(xiàn)了 IndexOf,那結(jié)果沒什么區(qū)別。但是如果 MyList 中沒有實現(xiàn) IndexOf 的時候,Java 和 C# 在處理上有就區(qū)別了。

先看看 C# 的 Main 函數(shù),編譯不過(Compiler Error CS1929),因為 MyList 中沒有定義 IndexOf。

C#中的接口默認方法是什么?

而 Java 呢?通過了,一如既往的運行出了結(jié)果!

C#中的接口默認方法是什么?

從 C# 的角度來看,MyList 既然知道有 IndexOf 接口,那就應該實現(xiàn)它,而不能假裝不知道。但是如果通過 IStringList 來調(diào)用 IndexOf,那么就可以認為 MyList 并不知道有 IndexOf 接口,因此允許調(diào)用默認接口。接口還是接口,不知道有新接口方法,沒實現(xiàn),不怪你;但是你明知道還不實現(xiàn),那就是你的不對了。

但從 Java 的角度來看,MyList 的消費者并不一定是 MyList 的生產(chǎn)者。從消費者的角度來看,MyList 實現(xiàn)了 StringList 接口,而接口定義有 indexOf 方法,所以消費者調(diào)用 myList.indexOf 是合理的。

Java 的行為相對寬松,只要有實現(xiàn)你就用,不要管是什么實現(xiàn)。

而 C# 的行為更為嚴格,消費者在使用的時候可以通過編譯器很容易了解到自己使用的是類實現(xiàn),還是接口中的默認實現(xiàn)(雖然知道了也沒多少用)。實際上,如果沒在在類里面實現(xiàn),接口文檔中就不會寫出來相關的接口,編輯器的智能提示也不會彈出來。實在要寫,可以顯示轉(zhuǎn)換為接口來調(diào)用:

Console.WriteLine(((IStringList)myList).IndexOf("Third"));

而且根據(jù)上面的試驗結(jié)果,將來 MyList 實現(xiàn)了 IndexOf 之后,這樣的調(diào)用會直接切換到調(diào)用 MyList 中的實現(xiàn),不會產(chǎn)生語義上的問題。

4. 問題二,關于多重繼承的問題

無論 Java 還是 C# 都不允許類多繼承,但是接口可以。而接口中的默認實現(xiàn)帶來了類似于類多繼承所產(chǎn)生的問題,怎么辦?

舉個例,人可以走,鳥也可以走,那么“云中君”該怎么走?

4.1. 先來看 C# 的

類中不實現(xiàn)默認接口的情況:

interface IPerson
{
    void Walk() => Console.WriteLine("IPerson.Walk()");
}

interface IBird
{
    void Walk() => Console.WriteLine("IBird.Walk()");
}

class BirdPerson : IPerson, IBird { }

調(diào)用結(jié)果:

BirdPerson birdPerson = new BirdPerson();
// birdPerson.Walk();           // CS1061,沒有實現(xiàn) Walk
((IPerson)birdPerson).Walk();   // 輸出 IPerson.Walk()
((IBird)birdPerson).Walk();     // 輸出 IBird.Walk()

不能直接使用 birdPerson.Walk(),道理前面已經(jīng)講過。不過通過不同的接口類型來調(diào)用,行為是不一致的,完全由接口的默認方法來決定。這也可以理解,既然類沒有自己的實現(xiàn),那么用什么接口來引用,說明開發(fā)者希望使用那個接口所規(guī)定的默認行為。

說得直白一點,你把云中君看作人,他就用人的走法;你把云中君看作鳥,它就用鳥的走法。

然而,如果類中有實現(xiàn),情況就不一樣了:

class BirdPerson : IPerson, IBird
{
    // 注意這里的 public 可不能少
    public void Walk() => Console.WriteLine("BirdPerson.Walk()");
}
BirdPerson birdPerson = new BirdPerson();
birdPerson.Test();              // 輸出 BirdPerson.Walk()
((IPerson)birdPerson).Walk();   // 輸出 BirdPerson.Walk()
((IBird)birdPerson).Walk();     // 輸出 BirdPerson.Walk()

輸出完全一致,接口中定義的默認行為,在類中有實現(xiàn)的時候,就當不存在!

云中君有個性:不管你怎么看,我就這么走。

這里唯一需要注意的是 BirdPerson 中實現(xiàn)的 Walk() 必須聲明為 public,否則 C# 會把它當作類的內(nèi)部行為,而不是實現(xiàn)的接口行為。這一點和 C# 對實現(xiàn)接口方法的要求是一致的:實現(xiàn)接口成員必須聲明為 public。

4.2. 接著看 Java 的不同

轉(zhuǎn)到 Java 這邊,情況就不同了,編譯根本不讓過

interface Person {
    default void walk() {
        out.println("IPerson.walk()");
    }
}

interface Bird {
    default void walk() {
        out.println("Bird.walk()");
    }
}

// Duplicate default methods named walk with the parameters () and ()
// are inherited from the types Bird and Person
class BirdPerson implements Person, Bird { }

這個意思就是,PersonBird 都為簽名相同的 walk 方法定義了默認現(xiàn),所以編譯器不知道 BirdPerson 到底該怎么辦了。那么如果只有一個 walk 有默認實現(xiàn)呢?

interface Person {
    default void walk() {
        out.println("IPerson.walk()");
    }
}

interface Bird {
    void walk();
}

// The default method walk() inherited from Person conflicts
// with another method inherited from Bird
class BirdPerson implements Person, Bird { }

這意思是,兩個接口行為不一致,編譯器還是不知道該怎么處理 BirdPerson。

總之,不管怎么樣,就是要 BirdPerson 必須實現(xiàn)自己的 walk()。既然 BirdPerson 自己實現(xiàn)了 walk(),那調(diào)用行為也就沒有什么懸念了:

BirdPerson birdPerson = new BirdPerson();
birdPerson.walk();              // 輸出 BirdPerson.walk()
((Person) birdPerson).walk();   // 輸出 BirdPerson.walk()
((Bird) birdPerson).walk();     // 輸出 BirdPerson.walk()

4.3. 結(jié)論,多繼承沒有問題

如果一個類實現(xiàn)的多個接口中定義了相同簽名的方法,沒有默認實現(xiàn)的情況下,當然不會有問題。

如果類中實現(xiàn)了這個簽名的方法,那無論如何,調(diào)用的都是這個方法,也不會有問題。

但在接口有默認實現(xiàn),而類中沒有實現(xiàn)的情況下,C# 將實際行為交給引用類型去處理;Java 則直接報錯,交給開發(fā)者去處理。筆者比較贊同 C# 的做法,畢竟默認方法的初衷就是為了不強制開發(fā)者去處理增加接口方法帶來的麻煩。

5. 問題三,更復雜的情況怎么去分析

對于更復雜的情況,多數(shù)時候還是可以猜到會怎么去調(diào)用的,畢竟有個基本原則在那里。

5.1. 在類中的實現(xiàn)優(yōu)先

比如,WalkBase 定義了 Walk() 方法,但沒實現(xiàn)任何接口,BirdPersonWalkBase 繼承,實現(xiàn)了 IPerson 接口,但沒實現(xiàn) Walk() 方法,那么該執(zhí)行哪個 Walk 呢?

會執(zhí)行 WalkBase.Walk()——不管什么情況下,類方法優(yōu)先!

class WalkBase
{
    public void Walk() => Console.WriteLine("WalkBase.Walk()");
}

class BirdPerson : WalkBase, IPerson { }

static void Main(string[] args)
{
    BirdPerson birdPerson = new BirdPerson();
    birdPerson.Walk();              // 輸出 WalkBase.Walk()
    ((IPerson)birdPerson).Walk();   // 輸出 WalkBase.Walk()
}

如果父類子類都有實現(xiàn),但子類不是“重載”,而是“覆蓋”實現(xiàn),那要根據(jù)引用類型來找最近的類,比如

class WalkBase : IBird  // <== 注意這里實現(xiàn)了 IBird
{
    public void Walk() => Console.WriteLine("WalkBase.Walk()");
}

class BirdPerson : WalkBase, IPerson  // <== 這里_沒有_實現(xiàn) IBird
{
    // 注意:這里是 new,而不是 override
    public new void Walk() => Console.WriteLine("BirdPerson.Walk()");
}

static void Main(string[] args)
{
    BirdPerson birdPerson = new BirdPerson();
    birdPerson.Walk();              // 輸出 BirdPerson.Walk()
    ((WalkBase)birdPerson).Walk();  // 輸出 WalkBase.Walk()
    ((IPerson)birdPerson).Walk();   // 輸出 BirdPerson.Walk()
    ((IBird)birdPerson).Walk();     // 輸出 WalkBase.Walk()
}

如果 WalkBase 中以 virtual 定義 Walk(),而 BirdPerson 中以 override 定義 Walk(),那毫無懸念輸出全都是 BirdPerson.Walk()。

class WalkBase : IBird
{
    public virtual void Walk() => Console.WriteLine("WalkBase.Walk()");
}

class BirdPerson : WalkBase, IPerson
{
    public override void Walk() => Console.WriteLine("BirdPerson.Walk()");
}

static void Main(string[] args)
{
    BirdPerson birdPerson = new BirdPerson();
    birdPerson.Walk();              // 輸出 BirdPerson.Walk()
    ((WalkBase)birdPerson).Walk();  // 輸出 BirdPerson.Walk()
    ((IPerson)birdPerson).Walk();   // 輸出 BirdPerson.Walk()
    ((IBird)birdPerson).Walk();     // 輸出 BirdPerson.Walk()
}

上面示例中的候最后一句輸出,是通過 IBird.Walk() 找到 WalkBase.Walk(),而 WalkBase.Walk() 又通過虛方法鏈找到 BirdPerson.Walk(),所以輸出仍然是 BirdPerson.Walk()。學過 C++ 的同學這時候可能就會很有感覺了!

至于 Java,所有方法都是虛方法。雖然可以通過 final 讓它非虛,但是在子類中不能定義相同簽名的方法,所以 Java 的情況會更簡單一些。

5.2. 類中無實現(xiàn),根據(jù)引用類型找最近的默認實現(xiàn)

還是拿 WalkBaseBirdPerson 分別實現(xiàn)了 IBirdIPerson 的例子,

class WalkBase : IBird { }
class BirdPerson : WalkBase, IPerson { }

((IPerson)birdPerson).Walk();   // 輸出 IPerson.Walk()
((IBird)birdPerson).Walk();     // 輸出 IBird.Walk()

哦,當然 Java 中不存在,因為編譯器會要求必須實現(xiàn) BirdPerson.Walk()


以上就是C#中接口默認方法的詳細內(nèi)容了,看完之后是否有所收獲呢?如果想了解更多相關內(nèi)容,歡迎關注億速云行業(yè)資訊!

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI