【C#本质论 八】类-从设计的角度去认知(多态)

简介: 【C#本质论 八】类-从设计的角度去认知(多态)

其实称本篇为多态还是有些牵强,因为在类的继承中也是存在多态的,例如我们的重写机制,但可以设想这样一个场景:飞行这个动作,鸟可以飞行,飞机可以飞行,而飞机其实和鸟没有父子关系的,他们共同拥有的是行为:飞行。所以本篇博客着重介绍这一点:如何通过接口来处理行为一致(横向关系)而非一脉相承(纵向关系)的关系。本篇的结构如下:

接口定义

为什么要有接口?在介绍了类和抽象类(我的感觉就是抽象类属于类和接口的中间产物,既有继承基类的能力,也有扩展抽象成员方法的能力)之后,我们知道,可以通过重写和实现抽象成员来扩展和组织自己的内容。那么接口有啥必要呢?,既然有类,那还要接口做什么呢?:

  • 接口不包含任何实现,可以完全隔离实现细节和提供的服务
  • 基类除了允许共享成员签名,还可以共享实现(派生类通过继承基类方法直接使用其实现),接口只允许共享成员签名,不允许共享实现(接口本来就不提供实现)
  • C#是单继承的,可以通过使用接口来扩展想要的实现方法,而且接口是多继承(实现)的

接口的定义如下,

interface IFileCompression
    {
        void Compress(string targetFileName, string[] fileList);
        void Uncompress(string compressedFileName, string expandDirectoryName);
    }

需要注意以下几点:

  • 接口不包含任何实现和数据,只包含属性(属性也不包含对应的支持字段,所以不能使用自动属性)和实例方法,并且不包含任何静态成员!
  • C#不允许接口使用访问修饰符,所有成员自动公共
  • 派生类实现接口时,也要用冒号,和基类以逗号分隔,需要注意的是,基类在前,接口顺序任意.
  • 接口的所有成员都必须实现。
  • 接口永远不能实例化,而且接口不像抽象类,连构造函数和终结器都没有。
  • 接口必须显示转换为它的某个实现类型,而因为它的派生类总是实现它的全部方法,所以可以隐式转换
  • 不要修改已经发布的接口,而是通过给该接口添加子接口来实现版本控制

抽象类如何通过接口甩锅呢?通过如下方式抽象类可以不实现接口方法而是让派生类实现。

public interface Ifoo
{
    void Bar();
}
public abstract class Foo : Ifoo
{
    public abstract void Bar();
}
public class Child : Foo
{
    public override void Bar()
    {
        throw new NotImplementedException();
    }
}

接口实现多态

如何使用接口实现多态,其实和隐式转型类似哦。

1,定义接口和抽象类

public interface IListable
    {
        // Return the value of each column in the row
        string[] ColumnValues
        {
            get;
        }
    }
    public abstract class PdaItem
    {
        public PdaItem(string name)
        {
            Name = name;
        }
        public virtual string Name { get; set; }
    }

2,定义两种不同的实现类

public class Contact : PdaItem, IListable
    {
        public Contact(string firstName, string lastName,
            string address, string phone)
            : base(null)
        {
            FirstName = firstName;
            LastName = lastName;
            Address = address;
            Phone = phone;
        }
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Address { get; set; }
        public string Phone { get; set; }
        public string[] ColumnValues
        {
            get
            {
                return new string[]
                {
                    FirstName,
                    LastName,
                    Phone,
                    Address
                };
            }
        }
        public static string[] Headers
        {
            get
            {
                return new string[] {
                    "First Name", "Last Name    ", 
                    "Phone       ",
                    "Address                       " };
            }
        }
        // ...
    }
    public class Publication : IListable
    {
        public Publication(string title, string author, int year)
        {
            Title = title;
            Author = author;
            Year = year;
        }
        public string Title { get; set; }
        public string Author { get; set; }
        public int Year { get; set; }
        public string[] ColumnValues
        {
            get
            {
                return new string[]
                {
                    Title,
                    Author,
                    Year.ToString()
                };
            }
        }
        public static string[] Headers
        {
            get
            {
                return new string[] {
                    "Title                                                    ", 
                    "Author             ", 
                    "Year" };
            }
        }
        // ...
    }

3,定义接口调用方的类和调用方法

public class ConsoleListControl
    {
        public static void List(string[] headers, IListable[] items)
        {
            int[] columnWidths = DisplayHeaders(headers);
            for(int count = 0; count < items.Length; count++)
            {
                string[] values = items[count].ColumnValues;
                DisplayItemRow(columnWidths, values);
            }
        }
     }

4,定义入口,方法实现

public static void Main()
        {
            Contact[] contacts = new Contact[]
            {
              new Contact(
                  "Dick", "Traci",
                  "123 Main St., Spokane, WA  99037",
                  "123-123-1234"),
              new Contact(
                  "Andrew", "Littman",
                  "1417 Palmary St., Dallas, TX 55555",
                  "555-123-4567"),
              new Contact(
                  "Mary", "Hartfelt",
                  "1520 Thunder Way, Elizabethton, PA 44444",
                  "444-123-4567"),
              new Contact(
                  "John", "Lindherst",
                  "1 Aerial Way Dr., Monteray, NH 88888",
                  "222-987-6543"),
              new Contact(
                  "Pat", "Wilson",
                  "565 Irving Dr., Parksdale, FL 22222",
                  "123-456-7890"),
              new Contact(
                  "Jane", "Doe",
                  "123 Main St., Aurora, IL 66666",
                  "333-345-6789")
            };
            // Classes are cast implicitly to
            // their supported interfaces
            ConsoleListControl.List(Contact.Headers, contacts);
            Console.WriteLine();
            Publication[] publications = new Publication[3] {
                new Publication(
                    "The End of Poverty: Economic Possibilities for Our Time",
                    "Jeffrey Sachs", 2006),
                new Publication("Orthodoxy", 
                    "G.K. Chesterton", 1908),
                new Publication(
                    "The Hitchhiker's Guide to the Galaxy",
                    "Douglas Adams", 1979)
                };
            ConsoleListControl.List(
                Publication.Headers, publications);
        }

隐式调用接口方法,会使用各个派生类对接口各自的实现方式:

Contact会实现如下内容:输出以下四个属性:FirstName、LastName、Phone、Address

Publication会实现如下内容:输出:Title、Author、Year

接口实现

分为显式和隐式、接口继承,以及多接口继承。

显式和隐式

接口其实分为两种实现,显式和隐式,上面介绍的实现是隐式的,感觉像是我们大多数时候使用的场景,但有些时候也会用到显式实现:

public class Program
    {
        public static void Main()
        {
            string[] values;
            Contact contact1, contact2 = null;
            // ...
            // ERROR:  Unable to call ColumnValues() directly
            //         on a contact
            // values = contact1.ColumnValues;
            // First cast to IListable
            values = ((IListable)contact2).ColumnValues;// 显式
            var result = contact1.CompareTo(contact2);// 隐式
        }
    }
    public class Contact : PdaItem, IListable, IComparable
    {
        // ...
        public Contact(string name)
            : base(name)
        {
        }
        #region IComparable Members
        public int CompareTo(object obj)  //隐式实现
        { 
            //...
        }
        #endregion
        #region IListable Members
        string[] IListable.ColumnValues    //显式实现
        {
            get
            {
                return new string[] 
          {
              FirstName,
              LastName,
              Phone,
              Address
          };
            }
        }
        #endregion
        protected string LastName { get; set; }
        protected string FirstName { get; set; }
        protected string Phone { get; set; }
        protected string Address { get; set; }
    }

显式实现values = ((IListable)contact2).ColumnValues;,显式实现方式只能通过接口本身调用!不能使用virtual、override或者public来修饰!

隐式实现var result = contact1.CompareTo(contact2); 隐式实现方式允许类自身调用!virtual、override或者public都是可选参数。

对于隐式和显式实现的接口成员,关键区别不在于成员声明的语法,而在于通过类型的实例而不是接口访问成员的能力。 建立类层次结构时需要建模真实世界的“属于”(is a)关系——例如,长颈鹿“属于”哺乳动物。这些是“语义”(semantic)关系。而接口用于建模“机制”(mechanism)关系。PdaItem“不属于”一种“可比较”(comparable)的东西,但它仍可实现IComparable接口。该接口和语义模型无关,只是实现机制的细节。显式接口实现的目的就是将“机制问题”和“模型问题”分开要求调用者先将对象转换为接口(比如IComparable),然后才能认为对象“可比较”,从而显式区分你想在什么时候和模型沟通,以及想在什么时候处理实现机制

一般来说,最好的做法是将一个类的公共层面限制成“全模型”,尽量少地涉及无关的机制

  • 成员是不是核心的类功能,如果是,使用隐式,这样把实现方法当成自己成员,如果不是,用显式。
  • 接口成员名称作为类成员名称是否恰当?假定ITrace接口的Dump()成员将类的数据写入跟踪日志。在Person或者Truck(卡车)类中隐式实现Dump()会混淆该方法的作用。所以,更好的选择是显式实现,确保只能通过ITrace数据类型调用Dump(),使该方法不会产生歧义。总之,假如成员的用途在实现类中不明确,就考虑显式实现。
  • **是否已经有相同签名的类成员?**显式接口成员实现不会在类型的声明空间添加具名元素。所以,如果类型已存在可能冲突的成员,那么显式接口可与之同签名

大多数情况我们使用隐式的方式

接口继承

接口可以多继承,但是需要注意的是,如果显式的实现接口,则必须使用最初声明该接口的名称。

namespace AddisonWesley.Michaelis.EssentialCSharp.Chapter08.Listing08_08
{
    interface IReadableSettingsProvider
    {
        string GetSetting(string name, string defaultValue);
    }
    interface ISettingsProvider : IReadableSettingsProvider
    {
        void SetSetting(string name, string value);
    }

这样调用Getting方法是不对的:

class FileSettingsProvider : ISettingsProvider,
        IReadableSettingsProvider
    {
        #region ISettingsProvider Members
        public void SetSetting(string name, string value)
        {
            // ...
        }
        #endregion
        #region IReadableSettingsProvider Members
         string ISettingsProvider.GetSetting(string name, string defaultValue)
        {
            return name + defaultValue; //just returning this for the example
        }
        #endregion
    }
}

应该这样调用:

string IReadableSettingsProvider.GetSetting(string name, string defaultValue)
        {
            return name + defaultValue; //just returning this for the example
        }

接口多继承

接口和类一样,也可以继承多个接口:

namespace AddisonWesley.Michaelis.EssentialCSharp.Chapter08.Listing08_09
{
    interface IReadableSettingsProvider
    {
        string GetSetting(string name, string defaultValue);
    }
    interface IWriteableSettingsProvider
    {
        void SetSetting(string name, string value);
    }
    interface ISettingsProvider : IReadableSettingsProvider,
        IWriteableSettingsProvider
    {
    }
}

接口附加扩展方法

接口也可以被当做扩展方法使用,而且更加灵活,比类更加实用,而且该扩展方法不仅可以是特定类型,还允许特定类型的集合。

static class Listable
    {
        public static void List(
            this IListable[] items, string[] headers)
        {
            int[] columnWidths = DisplayHeaders(headers);
            for(int itemCount = 0; itemCount < items.Length; itemCount++)
            {
                if (items[itemCount] != null)
                {
                    string[] values = items[itemCount].ColumnValues;
                    DisplayItemRow(columnWidths, values);
                }
            }
        }
}

接口和类

接口和类的一些比较,用途不同吧:

推荐这么使用二者:

  1. 一般要优先选择类而不是接口。用抽象类分离契约(类型做什么)与实现细节(类型怎么做)。
  2. 如果需要使已从其他类型派生的类型支持接口定义的功能,考虑定义接口。

其实二者各有千秋吧,一种是模型角度,一种是机制角度,一种纵向,一种横向。

相关文章
|
2月前
|
开发框架 .NET C#
C#|.net core 基础 - 删除字符串最后一个字符的七大类N种实现方式
【10月更文挑战第9天】在 C#/.NET Core 中,有多种方法可以删除字符串的最后一个字符,包括使用 `Substring` 方法、`Remove` 方法、`ToCharArray` 与 `Array.Copy`、`StringBuilder`、正则表达式、循环遍历字符数组以及使用 LINQ 的 `SkipLast` 方法。
|
3月前
|
存储 C# 索引
C# 一分钟浅谈:数组与集合类的基本操作
【9月更文挑战第1天】本文详细介绍了C#中数组和集合类的基本操作,包括创建、访问、遍历及常见问题的解决方法。数组适用于固定长度的数据存储,而集合类如`List<T>`则提供了动态扩展的能力。文章通过示例代码展示了如何处理索引越界、数组长度不可变及集合容量不足等问题,并提供了解决方案。掌握这些基础知识可使程序更加高效和清晰。
88 2
|
2月前
|
编译器 C#
C#多态概述:通过继承实现的不同对象调用相同的方法,表现出不同的行为
C#多态概述:通过继承实现的不同对象调用相同的方法,表现出不同的行为
127 65
|
3月前
|
安全 C#
C# 面向对象编程的三大支柱:封装、继承与多态
【9月更文挑战第17天】在C#中,面向对象编程的三大支柱——封装、继承与多态,对于编写安全、可维护、可复用的代码至关重要。封装通过访问修饰符和属性保护数据;继承允许子类继承父类的属性和方法,实现代码复用和多态;多态则提高了代码的灵活性和通用性。掌握这三大概念能显著提升C#编程能力,优化开发效率和代码质量。
|
2月前
|
Java 程序员 C#
【类的应用】C#应用之派生类构造方法给基类构造方法传参赋值
【类的应用】C#应用之派生类构造方法给基类构造方法传参赋值
15 0
|
3月前
|
C# 数据安全/隐私保护
C# 一分钟浅谈:类与对象的概念理解
【9月更文挑战第2天】本文从零开始详细介绍了C#中的类与对象概念。类作为一种自定义数据类型,定义了对象的属性和方法;对象则是类的实例,拥有独立的状态。通过具体代码示例,如定义 `Person` 类及其实例化过程,帮助读者更好地理解和应用这两个核心概念。此外,还总结了常见的问题及解决方法,为编写高质量的面向对象程序奠定基础。
33 2
|
4月前
|
C#
C#中的类和继承
C#中的类和继承
45 6
|
4月前
|
Java C# 索引
C# 面向对象编程(一)——类
C# 面向对象编程(一)——类
40 0
|
4月前
|
开发框架 .NET 编译器
C# 中的记录(record)类型和类(class)类型对比总结
C# 中的记录(record)类型和类(class)类型对比总结
|
6月前
|
开发框架 .NET 编译器
程序与技术分享:C#基础知识梳理系列三:C#类成员:常量、字段、属性
程序与技术分享:C#基础知识梳理系列三:C#类成员:常量、字段、属性
42 2