目录
前言
上一篇文章介绍了 c# 中基础的:数据类型、条件、循环、函数、委托、泛型 相关的内容。对于自定义的数据类型:类、结构,其非常重要的特性—— 派生 没有详细说明。
本篇文章将介绍 c# 中和 派生 相关的内容,包括:继承|派生、接口、属性、访问控制。
1. 继承
-
父类 & 子类
- 父类也称基类,子类也称派生类
- 父类派生出子类,或者说 子类继承自父类
- 比如一个简单的父类:动物,子类1:猫,子类2:狗
-
继承的用途
- 描述类的逻辑关系:在 面向对象 的编程语言中,一般都会提供 类、继承 相关的语法,用以描述 某一系列 抽象集合 的 is-a 关系。
- 代码复用:在派生类中可以仅重写于父类有差异的函数,达到对无差异的函数的复用
- 多态:基类引用子类的实例时,调用该引用的函数,实际调用的是实际子类对象的函数,也就是用基类类型可以实现对不同子类函数的访问。
-
代码样例(类继承)
-
基类
public class Animal { public string Name { get; set; } public Animal(string name) { Name = name; } public virtual void MakeSound() { Console.WriteLine("This animal makes a sound."); } }
-
派生类
public class Dog : Animal { public Dog(string name) : base(name) { } public override void MakeSound() { Console.WriteLine("Woof!"); } }
-
调用(多态)
public class Program { public static void Main() { Animal myAnimal = new Animal("Generic Animal"); myAnimal.MakeSound(); // 输出:This animal makes a sound. Dog myDog = new Dog("Buddy"); myDog.MakeSound(); // 输出:Woof! // 多态:通过基类引用调用派生类的方法 Animal myAnimalDog = new Dog("Max"); myAnimalDog.MakeSound(); // 输出:Woof! } }
-
-
c# 中的 “基类”
- 除了 class,在 c# 中还提供了 接口(Interface)用于定义接口,可以被继承。(在第2节详细介绍)
2. 接口
2.1 用途 & 优势
-
定义契约
- 接口定义了一组方法、属性、索引器或事件的签名,但不提供具体实现。实现接口的类或结构体必须提供这些成员的具体实现。
- 这种方式定义了类或结构体必须遵守的契约,确保它们具有一致的行为。
-
实现多态
- 多个类可以实现同一个接口,从而实现多态。通过接口引用,可以调用接口中定义的方法,而不关心具体实现。
- 这使得代码更加灵活,可以编写通用的代码来处理不同类型的对象。
-
解耦
接口可以减少类之间的耦合度,使得代码更加灵活和可维护。实现接口的类只需要关心接口定义的成员,而不需要知道其他类的具体实现。 -
代码复用
接口允许不同类共享相同的方法签名,从而提高代码的复用性。 -
扩展性
接口可以很容易地扩展。如果需要添加新的方法或属性,可以在接口中添加新的签名,而不需要修改现有的实现。
2.2 可以定义的内容
-
方法(Methods):
接口可以定义方法的签名,不提供具体实现(c# 8.0 后可以定义默认实现)。
实现接口的类或结构体必须提供这些方法的具体实现。 -
属性(Properties):
接口可以定义属性的签名,但不提供具体实现。
实现接口的类或结构体必须提供这些属性的具体实现。 -
索引器(Indexers):
接口可以定义索引器的签名,但不提供具体实现。
实现接口的类或结构体必须提供这些索引器的具体实现。 -
事件(Events):
接口可以定义事件的签名,但不提供具体实现。
实现接口的类或结构体必须提供这些事件的具体实现。
2.3 特点
- 接口本身
不能实例化
,只能通过实现接口的类或结构体来实例化。 - 接口中的
成员默认是 public,不能有访问修饰符
。 接口可以继承接口
,支持多继承。- 一个类可以实现多个接口,从而实现
多继承
的效果。 - 从
C# 8.0 开始,接口中的函数可以提供默认实现
,实现接口的类可以选择性地覆盖这些方法。
2.2 定义 & 继承
-
关键字
- interface
- 类似于 类的关键字 class
-
代码样例
-
接口定义
public interface IAnimal { void MakeSound(); } public interface INameable { string Name { get; set; } } public interface IPet : IAnimal, INameable { void Play(); }
注:上面这个例子的特点有:
① 接口多继承:IPet 接口继承了多个接口
② IAnimal 接口中不实现具体的方法
③ INameable 接口中定义了属性 -
继承:接口实现
public class Dog : IPet { public string Name { get; set; } public void MakeSound() { Console.WriteLine("Woof!"); } public void Play() { Console.WriteLine($"{Name} is playing."); } } public class Cat : IPet { public string Name { get; set; } public void MakeSound() { Console.WriteLine("Meow!"); } public void Play() { Console.WriteLine($"{Name} is playing."); } }
-
2.3 使用 & 多态
- 代码样例
public class Program
{
public static void Main()
{
IPet dog = new Dog { Name = "Buddy" };
IPet cat = new Cat { Name = "Whiskers" };
dog.MakeSound(); // 输出:Woof!
dog.Play(); // 输出:Buddy is playing.
cat.MakeSound(); // 输出:Meow!
cat.Play(); // 输出:Whiskers is playing.
}
}
3. 属性
3.1 用途
在 C# 中,属性(Property) 是一种特殊的成员,它允许你访问类或结构体中的私有字段,同时提供了一种更安全和灵活的方式来控制对字段的访问。属性通常用于封装字段,隐藏字段的实现细节,并提供额外的逻辑(如验证输入)。
3.2 语法
3.2.1 简写
- 代码样例
public class MyClass
{
// 定义一个自动实现的属性
public int Property { get; set; }
}
public class Program
{
public static void Main()
{
MyClass obj = new MyClass();
// 设置属性的值
obj.Property = 10;
// 获取属性的值
Console.WriteLine(obj.Property); // 输出:10
}
}
- 注:虽然不需要显式定义私有字段,但 编译器会自动生成 一个私有字段来存储属性的值。
3.2.2 完整写法:定义私有成员、实现 get|set
- 代码示例
public class MyClass
{
private int _property; // 私有字段
// 定义一个属性
public int Property
{
get { return _property; } // 返回私有字段的值
set
{
if (value < 0)
{
throw new ArgumentException("Value cannot be negative.");
}
_property = value; // 设置私有字段的值
}
}
}
public class Program
{
public static void Main()
{
MyClass obj = new MyClass();
// 设置属性的值
obj.Property = 10;
// 获取属性的值
Console.WriteLine(obj.Property); // 输出:10
// 尝试设置一个负值
try
{
obj.Property = -5;
}
catch (ArgumentException ex)
{
Console.WriteLine(ex.Message); // 输出:Value cannot be negative.
}
}
}
3.2.3 只读 & 只写 属性
-
只读:仅定义 get
-
代码样例
public class MyClass { private int _field; // 私有字段 // 只读属性 public int ReadOnlyProperty { get { return _field; } } // 公共方法用于设置只读属性的值 public void SetProperty(int value) { _field = value; // 通过私有字段设置值 } }
-
-
只写:仅定义 set
- 代码样例
public class MyClass { private int _field; // 私有字段 // 只写属性 public int WriteOnlyProperty { set { _field = value; } } // 公共方法用于获取只写属性的值 public int GetProperty() { return _field; // 通过私有字段获取值 } }
4. 索引器
4.1 用途
- 在 C# 中,索引器(Indexer) 是一种特殊的成员,它允许对象以类似数组的方式被索引。索引器可以提供基于整数、字符串或其他类型的索引访问,使得对象的使用更加灵活和直观。
- 索引器通常用于以下场景:
- 提供基于索引的访问:允许对象像数组或字典一样被访问。
- 封装内部数据结构:隐藏内部数据的实现细节,只暴露索引访问接口。
- 提供自定义的索引逻辑:可以基于不同的键类型(如字符串、整数等)提供自定义的索引访问。
4.2 语法
-
整型索引
-
代码样例
- 定义
public class MyCollection { private string[] _items = new string[10]; public string this[int index] { get { if (index < 0 || index >= _items.Length) { throw new IndexOutOfRangeException("Index out of range."); } return _items[index]; } set { if (index < 0 || index >= _items.Length) { throw new IndexOutOfRangeException("Index out of range."); } _items[index] = value; } } }
- 使用
public class Program { public static void Main() { MyCollection collection = new MyCollection(); // 设置值 collection[0] = "Item 1"; collection[1] = "Item 2"; // 获取值 Console.WriteLine(collection[0]); // 输出:Item 1 Console.WriteLine(collection[1]); // 输出:Item 2 } }
-
-
字符串索引
- 代码样例
- 定义
public class MyDictionary { private Dictionary<string, string> _data = new Dictionary<string, string>(); public string this[string key] { get { if (_data.ContainsKey(key)) { return _data[key]; } throw new KeyNotFoundException("Key not found."); } set { _data[key] = value; } } }
- 使用
public class Program { public static void Main() { MyDictionary dictionary = new MyDictionary(); // 设置值 dictionary["key1"] = "Value 1"; dictionary["key2"] = "Value 2"; // 获取值 Console.WriteLine(dictionary["key1"]); // 输出:Value 1 Console.WriteLine(dictionary["key2"]); // 输出:Value 2 } }
- 代码样例
5. 事件
5.1 用途
-
在 C# 中,事件(Event) 是一种特殊的成员,用于实现发布-订阅模式。事件允许类或对象在特定情况下通知其他对象,从而实现对象之间的解耦和交互。事件通常用于处理用户界面事件(如按钮点击、窗口关闭等),但也可以用于任何需要通知的场景。
-
事件的基本概念
-
发布者(Publisher):
定义事件的类或对象,负责触发事件。
通常包含一个或多个事件成员,以及触发事件的方法。 -
订阅者(Subscriber):
注册事件的类或对象,负责处理事件。
通过事件处理器(Event Handler)接收事件通知。 -
事件处理器(Event Handler):
一个方法,用于处理事件。
事件处理器的签名必须与事件的委托类型匹配。
-
5.2 语法
5.2.1 定义
- 事件(发布者)
-
事件的定义通常包括以下步骤:
定义一个委托(Delegate),用于指定事件处理器的签名。
定义一个事件成员,使用 event 关键字。
提供一个方法来触发事件。 -
代码示例
-
using System;
public class EventPublisher
{
// 定义委托
public delegate void MyEventHandler(object sender, EventArgs e);
// 定义事件
public event MyEventHandler MyEvent;
// 触发事件的方法
public void DoSomething()
{
Console.WriteLine("Doing something...");
// 触发事件
OnMyEvent();
}
// 触发事件的辅助方法
protected virtual void OnMyEvent()
{
MyEvent?.Invoke(this, EventArgs.Empty);
}
}
- 订阅者
- 订阅者需要注册事件,并提供事件处理器来处理事件。
- 代码示例
using System;
public class EventSubscriber
{
public void Subscribe(EventPublisher publisher)
{
// 注册事件
publisher.MyEvent += HandleEvent;
}
public void Unsubscribe(EventPublisher publisher)
{
// 取消注册事件
publisher.MyEvent -= HandleEvent;
}
// 事件处理器
public void HandleEvent(object sender, EventArgs e)
{
Console.WriteLine("Event handled by EventSubscriber.");
}
}
5.2.2 使用
- 代码示例
using System;
public class Program
{
public static void Main()
{
EventPublisher publisher = new EventPublisher();
EventSubscriber subscriber = new EventSubscriber();
// 注册事件
subscriber.Subscribe(publisher);
// 触发事件
publisher.DoSomething(); // 输出:Doing something... Event handled by EventSubscriber.
// 取消注册事件
subscriber.Unsubscribe(publisher);
// 再次触发事件(不会输出)
publisher.DoSomething(); // 输出:Doing something...
}
}
6. 访问控制
在 C# 中,访问控制用于限制代码的可见性和访问权限。C# 提供了五种访问修饰符,分别是 public
、private
、protected
、internal
和 protected internal
。每种访问修饰符都有其特定的用途和作用范围。
6.1 访问修饰符及其作用范围
-
public
- 作用范围:任何地方都可以访问。
- 用途:用于公开类、方法、属性等,使其可以被外部代码访问。
-
private
- 作用范围:仅在定义它的类或结构体内部可以访问。
- 用途:用于隐藏类或结构体的内部实现细节,防止外部代码直接访问。
-
protected
- 作用范围:仅在定义它的类或结构体及其派生类中可以访问。
- 用途:用于允许派生类访问基类的成员,但不允许外部代码访问。
-
internal
- 作用范围:仅在同一个程序集(Assembly)内可以访问。
- 用途:用于限制类或成员的访问范围,使其仅在当前项目内可见。
-
protected internal
- 作用范围:在同一个程序集内或派生类中可以访问。
- 用途:结合了
protected
和internal
的特性,允许在当前程序集内或派生类中访问。
6.2 访问控制的具体应用
- 类(Class)
类的访问修饰符决定了类的可见性。
public class PublicClass
{
// 公共类,任何地方都可以访问
}
private class PrivateClass
{
// 私有类,仅在定义它的类或结构体内部可以访问
}
internal class InternalClass
{
// 内部类,仅在同一个程序集内可以访问
}
- 成员(Members)
成员(如字段、方法、属性等)的访问修饰符决定了它们的可见性。
public class MyClass
{
public int PublicField; // 公共字段,任何地方都可以访问
private int PrivateField; // 私有字段,仅在类内部可以访问
protected int ProtectedField; // 受保护字段,仅在类及其派生类中可以访问
internal int InternalField; // 内部字段,仅在同一个程序集内可以访问
protected internal int ProtectedInternalField; // 受保护内部字段,仅在同一个程序集内或派生类中可以访问
}
- 方法(Methods)
方法的访问修饰符决定了它们的可见性。
public class MyClass
{
public void PublicMethod()
{
// 公共方法,任何地方都可以访问
}
private void PrivateMethod()
{
// 私有方法,仅在类内部可以访问
}
protected void ProtectedMethod()
{
// 受保护方法,仅在类及其派生类中可以访问
}
internal void InternalMethod()
{
// 内部方法,仅在同一个程序集内可以访问
}
protected internal void ProtectedInternalMethod()
{
// 受保护内部方法,仅在同一个程序集内或派生类中可以访问
}
}
- 属性(Properties)
属性的访问修饰符决定了它们的可见性。属性的get
和set
访问器也可以有不同的访问修饰符。
public class MyClass
{
public int PublicProperty { get; set; } // 公共属性,任何地方都可以访问
private int PrivateProperty { get; set; } // 私有属性,仅在类内部可以访问
protected int ProtectedProperty { get; set; } // 受保护属性,仅在类及其派生类中可以访问
internal int InternalProperty { get; set; } // 内部属性,仅在同一个程序集内可以访问
protected internal int ProtectedInternalProperty { get; set; } // 受保护内部属性,仅在同一个程序集内或派生类中可以访问
// 属性的访问器可以有不同的访问修饰符
public int ReadOnlyProperty { get; private set; } // 只读属性,外部只能读取,不能设置
public int WriteOnlyProperty { private get; set; } // 只写属性,外部只能设置,不能读取
}
- 接口(Interfaces)
接口的成员(方法、属性等)默认是public
,不能显式指定访问修饰符。接口本身可以使用public
、internal
或protected internal
修饰符。
public interface IMyInterface
{
void Method(); // 默认是 public
int Property { get; set; } // 默认是 public
}
6.3 访问控制的具体示例
- 类和成员
public class MyClass
{
public int PublicField; // 公共字段
private int PrivateField; // 私有字段
protected int ProtectedField; // 受保护字段
internal int InternalField; // 内部字段
protected internal int ProtectedInternalField; // 受保护内部字段
public void PublicMethod() // 公共方法
{
Console.WriteLine("Public method");
}
private void PrivateMethod() // 私有方法
{
Console.WriteLine("Private method");
}
protected void ProtectedMethod() // 受保护方法
{
Console.WriteLine("Protected method");
}
internal void InternalMethod() // 内部方法
{
Console.WriteLine("Internal method");
}
protected internal void ProtectedInternalMethod() // 受保护内部方法
{
Console.WriteLine("Protected internal method");
}
public int PublicProperty { get; set; } // 公共属性
private int PrivateProperty { get; set; } // 私有属性
protected int ProtectedProperty { get; set; } // 受保护属性
internal int InternalProperty { get; set; } // 内部属性
protected internal int ProtectedInternalProperty { get; set; } // 受保护内部属性
}
- 派生类
public class DerivedClass : MyClass
{
public void Test()
{
PublicMethod(); // 可以访问
// PrivateMethod(); // 错误:私有方法不可访问
ProtectedMethod(); // 可以访问
// InternalMethod(); // 错误:内部方法不可访问(除非在同一程序集中)
ProtectedInternalMethod(); // 可以访问
}
}