前言
算一算很长时间没有写过文章了。不过隔壁出了个Java的抽象类与接口,那我当然很有兴趣来写写C#的抽象类和接口啦。本文意在于介绍抽象类以及接口的配合使用,关于更多细节请参考微软官方文档。
抽象类
使用abstract修饰的类即为抽象类,它具有以下语法特征:
1.无法创造实例,也就是说你没法new一个抽象基类出来。
2.只有抽象类可以包含抽象方法,即子类必须重写的方法。
在这里,我们定义一个动物的抽象类Animal,实现获取名称和被食用的功能(虽然这两个功能放一起感觉怪怪的)。
public abstract class Animal
{
//名称
protected string name;
//获取名称
public abstract string Name { get; }
//构造方法
public Animal(string name) => this.name = name;
//食用
public abstract void ToEat();
//打印名称
public virtual void PrintName()
{
System.Console.WriteLine("芝士动物");
}
}
然后,我们再写一个鸭子Duck类继承Animal类。如果不想让其他类继承Duck类,还可以使用sealed修饰此类为密封类。不过与抽象类才能包含抽象方法不同的是,即使不是密封类也可以包含密封方法。
public class Duck:Animal
{
//构造方法
public Duck(string name) : base(name) { }
//对抽象属性的实现
public override string Name => "鸭子" + name;
//使用sealed修饰,可以使得子类无法重写此方法(但是可以new覆盖)
//也就是说,鸭子只能烤着吃!(暴论)
public sealed override void ToEat()
{
System.Console.WriteLine("做成烤鸭吃掉了");
}
}
但只使用抽象类是有问题的:
1.并非只有动物才能获取名称和食用。换句话说,获取名称和食用这两个行为是广泛存在的,使用者只关注能不能获取名称以及能不能食用,而不关注它是动物,是水果还是巧克力。
//这里以食用为例
public class People
{
//吃小动物
public void Eat(Animal animal)
{
animal.ToEat();
}
//吃水果
public void Eat(Fruit fruit)
{
fruit.Eat(number: 2);
}
//吃巧克力
public void Eat(Chocolate chocolate)
{
chocolate.WhenEat();
}
//吃毛豆精
public void Eat(Zundamon nanoda)
{
nanoda.Used(times: 114);
}
//有多少种能吃的东西,就要补充多少个方法,每种东西吃的方法名称和参数可能还不一样……
//多了,少了一种东西,或者吃的方法变了,还得一个一个一个找和改。实在太痛苦了!太痛苦了!
}
2.一个类最多只能继承一个类,而我们往往很难只靠继承去定义一个类。因为继承造成的耦合性是极大的,依赖继承去设计合理的类之间的关系,难度不亚于用勺子吃面。
//小鸡仔是鸟类,但还不会飞,也不会下蛋。是幼崽,需要被照顾,但又会自己觅食。会游泳,但游泳很容易受凉去世。
public class Chick:Bird?FlyAnimal?Baby?SwimAnimal?
接口
不难发现,当我们通过”是什么“和”能做什么“去选择继承的对象时,很容易出问题。因为继承只能在描述”是什么“与”能做什么“之间选一个。于是,我们需要接口来辅助描述一个类。
接口使用interface定义,它具有以下语法特征:
1.无法创造实例,也就是说你没法new一个接口出来。
2.一个类可以同时实现多个接口,接口之间也可以多继承
3.接口中不能包含实例字段
尽管接口中不能包含实例字段,但接口既可以描述”是什么“,也可以描述”能做什么“。
前者是因为接口中虽然不能包含实例字段,但可以通过定义属性去描述,更甚者,可以直接通过接口的名称去判断。后者在于实现接口的类必须实现接口中的所有方法(除默认实现和显式实现外,一般情况下,包含同名同参方法即视为实现)。
namespace System.Collections.Generic
{
[DefaultMember("Item")]
public interface IDictionary<TKey, TValue> : ICollection<KeyValuePair<TKey, TValue>>, IEnumerable<KeyValuePair<TKey, TValue>>, IEnumerable
{
TValue this[TKey key] { get; set; }
ICollection<TKey> Keys { get; }
ICollection<TValue> Values { get; }
void Add(TKey key, TValue value);
bool ContainsKey(TKey key);
bool Remove(TKey key);
bool TryGetValue(TKey key, out TValue value);
}
}
可以说,接口的使用可以让我们转变一个思路,即:判断一个类是什么、能做什么不取决于这个类本身,而取决于它实现了什么接口。
使用
实际使用过程中,可以用抽象类配合接口的方式设计。这里,我们以此方式重新设计一下Animal和Duck类。
public interface IEdible
{
void ToEat();
}
public interface INamed
{
string Name { get; }
void PrintName();
}
public abstract class Animal:IEdible,INamed
{
protected string name;
//名称
public virtual string Name { get => name; protected set => name = value; }
//构造方法
public Animal(string name) => Name = name;
//食用
public abstract void ToEat();
//打印名称
public virtual void PrintName()
{
System.Console.WriteLine(Name);
}
}
public class Duck:Animal
{
//构造方法
public Duck(string name) : base(name) { }
//重写
public override string Name { get => name; protected set { name = "鸭子 " + value; } }
//怎么吃
public override void ToEat()
{
System.Console.WriteLine("做成烤鸭");
}
}
让我们做个测试,看看其是否能成功运行。这里我使用Unity进行测试。
//测试代码
Duck d = new Duck("小个子");
Debug.Log(d.Name);
Debug.Log("IsEdible = " + (d is IEdible));
//输出
[23:05:12]鸭子 小个子
[23:05:12]IsEdible = True
那么在经过设计以后,无论以后还有什么动物、水果、巧克力……我们的使用者都可以像这样简单粗暴的去使用,而不需要修改。
public class People
{
public void DoSomeThing(object obj)
{
//实现了IEdiable接口,就说明能吃,吃一下
if (obj is IEdible food) Eat(food);
//实现了IName接口,就说明能看,看一下
if (obj is INamed thing) See(thing);
}
public void Eat(IEdible food)=> food.ToEat();
public void See(INamed thing)=>System.Console.WriteLine($"看见了{thing.Name}");
}
(顺便感慨一下 is 运算符真的是个超级好用的东西,除了判空的时候)
对于小鸡仔,用接口重新设计可以是这样的:
public interface IBird:IFly,IWarble
public interface IChicken:IBird,ISwim,IWalk
public class Chick:Animal,IChicken,IBaby<IChicken>
//当然,这只是万千种方式之一。
//具体怎么设计要看你怎么使用。如果你的设想中小鸡仔和一般鸡只有行为不同,其他类似,那我更推荐使用策略模式,而不要单独设一个类。
更多
关于接口,随C#版本更新也有了些新特性,这也是我上文没有提到其某些”特征“的原因。
public interface IEdible
{
//C#8.0新特性,允许接口中方法的默认实现
void NewToEat()
{
System.Console.WriteLine("生吃");
}
//C#8.0起,接口中允许有静态方法的默认实现
static void NewStaicFunc() { System.Console.WriteLine("因为有默认实现,开发者反而很容易忽略掉它"); }
//C#11.0新特性,使得所有实现接口的类,必须实现这个静态方法
static abstract void NewStaticFunc();
}
另外,和Java一样,接口还有一个令人匪夷所思的设定:类中可以定义接口,接口中可以定义类。当然,类中也可以定义类,接口中也可以定义接口。所以就可以写个这种玩意出来:
public class A1
{
public class A11 { }
public interface I12
{
public interface I121 { }
public class A122
{
public class A1221 { }
public interface I1222
{
public class A12221
{
//无限套娃……
}
}
}
}
}
官方这样设计肯定是自有用意。所以只要写代码的人不是我的同事,或者我永远不会处理这段代码,我是很乐意看到他这样套娃的。
除此以外,接口对于泛型类对象有奇效。例如有一个object对象,你只知道它是已知的泛型类型,而不知道具体类型,你就无法调用它的泛型方法(因为你不知道具体类型,无法简单地将它转化为泛型类型对象)。但是,除了写一大段反射以外,你还可以通过转换成接口的形式去调用它的泛型方法,这就是为什么泛型类型往往要实现很多个接口。
总结
抽象基类能对所有派生类实现一定程度上的约束;而接口帮助我们描述一个类,或着多个类之间的共性。如果多个类之间具有相同的功能,便可以提炼出接口。使用抽象类配合接口,可以帮助我们更好地设计自己的程序,减少后期维护的成本。
文章版权归作者所有






- 最新
- 最热
查看全部