C#中的接口默認(rèn)方法是什么?

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

我們提供的服務(wù)有:網(wǎng)站設(shè)計(jì)、成都網(wǎng)站建設(shè)、微信公眾號(hào)開(kāi)發(fā)、網(wǎng)站優(yōu)化、網(wǎng)站認(rèn)證、襄陽(yáng)ssl等。為1000多家企事業(yè)單位解決了網(wǎng)站和推廣的問(wèn)題。提供周到的售前咨詢(xún)和貼心的售后服務(wù),是有科學(xué)管理、有技術(shù)的襄陽(yáng)網(wǎng)站制作公司

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

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

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

這個(gè) IStringList 作為一個(gè)基礎(chǔ)接口在類(lèi)庫(kù)中發(fā)布之后,大量的程序員使用了這個(gè)接口,實(shí)現(xiàn)了一堆各種各種各樣的列表,像 StringArrayList、LinkedStringList、StringQueue、StringStackSortedStringList……有抽象類(lèi),有擴(kuò)展接口,也有各種實(shí)現(xiàn)類(lèi)??傊?jīng)過(guò)較長(zhǎng)一段時(shí)間的積累,IStringList 的子孫遍布全球。

然后 IStringList 的發(fā)明者,決定為列表定義更多的方法,以適合在技術(shù)飛速發(fā)展下開(kāi)發(fā)者們對(duì) 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; } // 獲取列表長(zhǎng)度
}

當(dāng)然,接口變化之外所有實(shí)現(xiàn)類(lèi)都必須實(shí)現(xiàn)它,不然編譯器會(huì)報(bào)錯(cuò),基礎(chǔ)庫(kù)的抽象類(lèi) AbstractStringList 中實(shí)現(xiàn)了上述新增加的接口。整個(gè)基礎(chǔ)庫(kù)完美編譯,發(fā)布了 2.0 版本。

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

基礎(chǔ)庫(kù)的用戶(hù)們(開(kāi)發(fā)者)發(fā)出了極大的報(bào)怨聲,因?yàn)樗麄兲啻a編譯不過(guò)了!

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

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

2. 辦法

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

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

不得不說(shuō) C# 的擴(kuò)展方法很聰明,但它畢竟不是真正對(duì)接口進(jìn)行擴(kuò)展,所以在 C# 8 中也加入了默認(rèn)方法來(lái)解決接口擴(kuò)展造成的問(wèn)題。

接口擴(kuò)展方法提出來(lái)之后,雖然解決了默認(rèn)實(shí)現(xiàn)的問(wèn)題,卻又帶出了新的問(wèn)題。

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

3. 問(wèn)題一,默認(rèn)方法和類(lèi)實(shí)現(xiàn)方法的關(guān)系

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

3.1. 先來(lái)看看默認(rèn)方法的語(yǔ)法

  • 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 的接口都寫(xiě)出來(lái),主要是因?yàn)槎咧v法和命名規(guī)范略有不同。接下來(lái)進(jìn)行的研究 C# 和 Java 行為相似的地方,就主要以 C# 為例了。

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

接下來(lái)的實(shí)現(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 沒(méi)有實(shí)現(xiàn) IndexOf,但是使用起來(lái)不會(huì)有任何問(wèn)題

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 大小寫(xiě)
    }
}

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

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

// 這里用 partial class 表示是部分實(shí)現(xiàn),
// 對(duì)不住 javaer,Java 沒(méi)有部分類(lèi)語(yǔ)法
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# 作為示例,其實(shí) Java 也是一樣的。上面的示例中是通過(guò)接口類(lèi)型來(lái)調(diào)用的 IndexOf 方法。第一次調(diào)用的是 IStringList.IndexOf 默認(rèn)實(shí)現(xiàn),因?yàn)檫@時(shí)候 MyList 并沒(méi)有實(shí)現(xiàn) IndexOf;第二次調(diào)用的是 MyList.IndexOf 實(shí)現(xiàn)。筆者使用 Java 寫(xiě)了類(lèi)似的代碼,行為完全一致。

因此,對(duì)于默認(rèn)方法,會(huì)優(yōu)先調(diào)用類(lèi)中的實(shí)現(xiàn),如果類(lèi)中沒(méi)有實(shí)現(xiàn)具有默認(rèn)方法的接口,才會(huì)去調(diào)用接口中的默認(rèn)方法。

但是?。?!前面的示例是使用的接口類(lèi)型引用實(shí)現(xiàn),如果換成實(shí)例類(lèi)類(lèi)型來(lái)引用實(shí)例呢?

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

先看看 C# 的 Main 函數(shù),編譯不過(guò)(Compiler Error CS1929),因?yàn)?MyList 中沒(méi)有定義 IndexOf。

C#中的接口默認(rèn)方法是什么?

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

C#中的接口默認(rèn)方法是什么?

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

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

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

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

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

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

4. 問(wèn)題二,關(guān)于多重繼承的問(wèn)題

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

舉個(gè)例,人可以走,鳥(niǎo)也可以走,那么“云中君”該怎么走?

4.1. 先來(lái)看 C# 的

類(lèi)中不實(shí)現(xiàn)默認(rè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,沒(méi)有實(shí)現(xiàn) Walk
((IPerson)birdPerson).Walk();   // 輸出 IPerson.Walk()
((IBird)birdPerson).Walk();     // 輸出 IBird.Walk()

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

說(shuō)得直白一點(diǎn),你把云中君看作人,他就用人的走法;你把云中君看作鳥(niǎo),它就用鳥(niǎo)的走法。

然而,如果類(lèi)中有實(shí)現(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()

輸出完全一致,接口中定義的默認(rèn)行為,在類(lèi)中有實(shí)現(xiàn)的時(shí)候,就當(dāng)不存在!

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

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

4.2. 接著看 Java 的不同

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

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 { }

這個(gè)意思就是,PersonBird 都為簽名相同的 walk 方法定義了默認(rèn)現(xiàn),所以編譯器不知道 BirdPerson 到底該怎么辦了。那么如果只有一個(gè) walk 有默認(rèn)實(shí)現(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 { }

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

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

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

4.3. 結(jié)論,多繼承沒(méi)有問(wèn)題

如果一個(gè)類(lèi)實(shí)現(xiàn)的多個(gè)接口中定義了相同簽名的方法,沒(méi)有默認(rèn)實(shí)現(xiàn)的情況下,當(dāng)然不會(huì)有問(wèn)題。

如果類(lèi)中實(shí)現(xiàn)了這個(gè)簽名的方法,那無(wú)論如何,調(diào)用的都是這個(gè)方法,也不會(huì)有問(wèn)題。

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

5. 問(wèn)題三,更復(fù)雜的情況怎么去分析

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

5.1. 在類(lèi)中的實(shí)現(xiàn)優(yōu)先

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

會(huì)執(zhí)行 WalkBase.Walk()——不管什么情況下,類(lèi)方法優(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()
}

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

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

class BirdPerson : WalkBase, IPerson  // <== 這里_沒(méi)有_實(shí)現(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(),那毫無(wú)懸念輸出全都是 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()
}

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

至于 Java,所有方法都是虛方法。雖然可以通過(guò) final 讓它非虛,但是在子類(lèi)中不能定義相同簽名的方法,所以 Java 的情況會(huì)更簡(jiǎn)單一些。

5.2. 類(lèi)中無(wú)實(shí)現(xiàn),根據(jù)引用類(lèi)型找最近的默認(rèn)實(shí)現(xiàn)

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

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

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

哦,當(dāng)然 Java 中不存在,因?yàn)榫幾g器會(huì)要求必須實(shí)現(xiàn) BirdPerson.Walk()。


以上就是C#中接口默認(rèn)方法的詳細(xì)內(nèi)容了,看完之后是否有所收獲呢?如果想了解更多相關(guān)內(nèi)容,歡迎關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊!

分享標(biāo)題:C#中的接口默認(rèn)方法是什么?
路徑分享:http://muchs.cn/article42/jpighc.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供App設(shè)計(jì)、定制網(wǎng)站、網(wǎng)站內(nèi)鏈微信公眾號(hào)、品牌網(wǎng)站制作靜態(tài)網(wǎng)站

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶(hù)投稿、用戶(hù)轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話(huà):028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)

成都網(wǎng)站建設(shè)公司