public struct ValueStructure : ValueType // Generates CS0527.
除了它们可能通过单一继承继承继承的任何类型外,类型系统中的所有 .NET 类型都隐式继承自 Object 或从中派生的类型。 Object 的常用功能可用于任何类型。
为了说明隐式继承的具体含义,让我们来定义一个新类 SimpleClass
,这只是一个空类定义:
public class SimpleClass
然后可以使用反射(便于检查类型的元数据,从而获取此类型的相关信息),获取 SimpleClass
类型的成员列表。 尽管没有在 SimpleClass
类中定义任何成员,但示例输出表明它实际上有九个成员。 这些成员的其中之一是由 C# 编译器自动为 SimpleClass
类型提供的无参数(或默认)构造函数。 其余八个是 类型的成员 Object,类型系统中所有类和接口最终从中 .NET 隐式继承。
using System.Reflection;
public class SimpleClassExample
public static void Main()
Type t = typeof(SimpleClass);
BindingFlags flags = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public |
BindingFlags.NonPublic | BindingFlags.FlattenHierarchy;
MemberInfo[] members = t.GetMembers(flags);
Console.WriteLine($"Type {t.Name} has {members.Length} members: ");
foreach (MemberInfo member in members)
string access = "";
string stat = "";
var method = member as MethodBase;
if (method != null)
if (method.IsPublic)
access = " Public";
else if (method.IsPrivate)
access = " Private";
else if (method.IsFamily)
access = " Protected";
else if (method.IsAssembly)
access = " Internal";
else if (method.IsFamilyOrAssembly)
access = " Protected Internal ";
if (method.IsStatic)
stat = " Static";
string output = $"{member.Name} ({member.MemberType}): {access}{stat}, Declared by {member.DeclaringType}";
Console.WriteLine(output);
// The example displays the following output:
// Type SimpleClass has 9 members:
// ToString (Method): Public, Declared by System.Object
// Equals (Method): Public, Declared by System.Object
// Equals (Method): Public Static, Declared by System.Object
// ReferenceEquals (Method): Public Static, Declared by System.Object
// GetHashCode (Method): Public, Declared by System.Object
// GetType (Method): Public, Declared by System.Object
// Finalize (Method): Internal, Declared by System.Object
// MemberwiseClone (Method): Internal, Declared by System.Object
// .ctor (Constructor): Public, Declared by SimpleClass
由于隐式继承自 Object 类,因此 SimpleClass
类可以使用下面这些方法:
公共 ToString
方法将 SimpleClass
对象转换为字符串表示形式,返回完全限定的类型名称。 在这种情况下,ToString
方法返回字符串“SimpleClass”。
三个用于测试两个对象是否相等的方法:公共实例 Equals(Object)
方法、公共静态 Equals(Object, Object)
方法和公共静态 ReferenceEquals(Object, Object)
方法。 默认情况下,这三个方法测试的是引用相等性;也就是说,两个对象变量必须引用同一个对象,才算相等。
公共 GetHashCode
方法:计算允许在经哈希处理的集合中使用类型实例的值。
公共 GetType
方法:返回表示 SimpleClass
类型的 Type 对象。
受保护 Finalize 方法:用于在垃圾回收器回收对象的内存之前释放非托管资源。
受保护 MemberwiseClone 方法:创建当前对象的浅表复制。
由于是隐式继承,因此可以调用 SimpleClass
对象中任何继承的成员,就像它实际上是 SimpleClass
类中定义的成员一样。 例如,下面的示例调用 SimpleClass
从 Object 继承而来的 SimpleClass.ToString
方法。
public class EmptyClass
public class ClassNameExample
public static void Main()
EmptyClass sc = new();
Console.WriteLine(sc.ToString());
// The example displays the following output:
// EmptyClass
下表列出了可以在 C# 中创建的各种类型及其隐式继承自的类型。 每个基类型通过继承向隐式派生的类型提供一组不同的成员。
隐式继承自
继承和“is a”关系
通常情况下,继承用于表示基类和一个或多个派生类之间的“is a”关系,其中派生类是基类的特定版本;派生类是基类的具体类型。 例如,Publication
类表示任何类型的出版物,Book
和 Magazine
类表示出版物的具体类型。
一个类或结构可以实现一个或多个接口。 虽然接口实现代码通常用作单一继承的解决方法或对结构使用继承的方法,但它旨在表示接口与其实现类型之间的不同关系(即“can do”关系),而不是继承关系。 接口定义了其向实现类型提供的一部分功能(如测试相等性、比较或排序对象,或支持区域性敏感的分析和格式设置)。
请注意,“is a”还表示类型与其特定实例化之间的关系。 在以下示例中,Automobile
类包含三个唯一只读属性:Make
(汽车制造商)、Model
(汽车型号)和 Year
(汽车出厂年份)。 Automobile
类还有一个自变量被分配给属性值的构造函数,并将 Object.ToString 方法重写为生成唯一标识 Automobile
实例(而不是 Automobile
类)的字符串。
public class Automobile
public Automobile(string make, string model, int year)
if (make == null)
throw new ArgumentNullException(nameof(make), "The make cannot be null.");
else if (string.IsNullOrWhiteSpace(make))
throw new ArgumentException("make cannot be an empty string or have space characters only.");
Make = make;
if (model == null)
throw new ArgumentNullException(nameof(model), "The model cannot be null.");
else if (string.IsNullOrWhiteSpace(model))
throw new ArgumentException("model cannot be an empty string or have space characters only.");
Model = model;
if (year < 1857 || year > DateTime.Now.Year + 2)
throw new ArgumentException("The year is out of range.");
Year = year;
public string Make { get; }
public string Model { get; }
public int Year { get; }
public override string ToString() => $"{Year} {Make} {Model}";
在这种情况下,不得依赖继承来表示特定汽车品牌和型号。 例如,不需要定义 Packard
类型来表示帕卡德制造的汽车。 相反,可以通过创建将相应值传递给其类构造函数的 Automobile
对象来进行表示,如以下示例所示。
using System;
public class Example
public static void Main()
var packard = new Automobile("Packard", "Custom Eight", 1948);
Console.WriteLine(packard);
// The example displays the following output:
// 1948 Packard Custom Eight
基于继承的“is a”关系最适用于基类和向基类添加附加成员或需要基类没有的其他功能的派生类。
设计基类及其派生类
让我们来看看如何设计基类及其派生类。 在此部分中,将定义一个基类 Publication
,用于表示任何类型的出版物,如书籍、杂志、报纸、期刊、文章等。还将定义一个从 Publication
派生的 Book
类。 可以将示例轻松扩展为定义其他派生类,如 Magazine
、Journal
、Newspaper
和 Article
。
Publication 基类
设计 Publication
类时,需要做出下面几项设计决策:
要在 Publication
基类中添加哪些成员、Publication
成员是否提供方法实现或 Publication
是否是用作派生类模板的抽象基类。
在此示例中,Publication
类提供方法实现代码。 设计抽象基类及其派生类部分中的示例就使用抽象基类定义派生类必须重写的方法。 派生类可以随时提供适合派生类型的任意实现代码。
能够重用代码(即多个派生类共用基类方法的声明和实现代码,无需重写它们)是非抽象基类的优势所在。 因此,如果代码可能由某些或大多数特定 Publication
类型共用,则应向 Publication
添加成员。 如果无法有效地提供基类实现,则最终将不得不在派生类中提供基本相同的成员实现代码,而不是共用基类中的同一实现代码。 如果需要在多个位置保留重复的代码,可能会导致 bug 出现。
为了最大限度地提高代码重用性并创建合乎逻辑的直观继承层次结构,需要确保在 Publication
类中只添加所有或大多数出版物通用的数据和功能。 然后,派生类可以实现所表示的特定出版物种类的唯一成员。
类层次结构的扩展空间大小。 是否要开发包含三个或更多类的层次结构,而不是仅包含一个基类和一个或多个派生类? 例如,Publication
可以是 Periodical
的基类,而后者又是 Magazine
、Journal
和 Newspaper
的基类。
在示例中,将使用包含 Publication
类和一个派生类 Book
的小型层次结构。 可以轻松扩展此示例,使其可以创建其他许多派生自 Publication
的类,如 Magazine
和 Article
。
能否实例化基类。 如果不可以,则应向类应用 abstract 关键字。 否则,可通过调用类构造函数来实例化 Publication
类。 如果尝试通过直接调用类构造函数来实例化标记有 abstract
关键字的类,则 C# 编译器会生成错误 CS0144:“无法创建抽象类或接口的实例。”如果尝试使用反射进行类实例化,那么反射方法会引发 MemberAccessException。
默认情况下,可以通过调用类构造函数来实例化基类。 无需显式定义类构造函数。 如果基类的源代码中没有类构造函数,C# 编译器会自动提供默认的(无参数)构造函数。
在此示例中,将把 Publication
类标记为 Publication
,使其无法实例化。 一个不具备任何 abstract
方法的 abstract
类表示该类代表一个在几个具体类(例如 Book
和 Journal
)之间共享的抽象概念。
派生类是否必须继承特定成员的基类实现代码、是否能选择重写基类实现代码或者是否必须提供实现代码。 使用 abstract 关键字来强制派生类提供实现代码。 使用 virtual 关键字来允许派生类重写基类方法。 默认情况下,不可重写基类中定义的方法。
Publication
类不具备任何 abstract
方法,不过类本身是 abstract
。
派生类是否表示继承层次结构中的最终类,且本身不能用作其他派生类的基类。 默认情况下,任何类都可以用作基类。 可以应用 sealed 关键字来指明类不能用作其他任何类的基类。 尝试从密封类派生生成的编译器错误 CS0509,“无法从密封类型 <typeName> 派生”。
在此示例中,将把派生类标记为 sealed
。
以下示例展示了 Publication
类的源代码,以及 Publication.PublicationType
属性返回的 PublicationType
枚举。 除了继承自 Object 的成员之外,Publication
类还定义了以下唯一成员和成员重写:
public enum PublicationType { Misc, Book, Magazine, Article };
public abstract class Publication
private bool _published = false;
private DateTime _datePublished;
private int _totalPages;
public Publication(string title, string publisher, PublicationType type)
if (string.IsNullOrWhiteSpace(publisher))
throw new ArgumentException("The publisher is required.");
Publisher = publisher;
if (string.IsNullOrWhiteSpace(title))
throw new ArgumentException("The title is required.");
Title = title;
Type = type;
public string Publisher { get; }
public string Title { get; }
public PublicationType Type { get; }
public int Pages
get { return _totalPages; }
if (value <= 0)
throw new ArgumentOutOfRangeException(nameof(value), "The number of pages cannot be zero or negative.");
_totalPages = value;
public string GetPublicationDate()
if (!_published)
return "NYP";
return _datePublished.ToString("d");
public void Publish(DateTime datePublished)
_published = true;
_datePublished = datePublished;
if (string.IsNullOrWhiteSpace(copyrightName))
throw new ArgumentException("The name of the copyright holder is required.");
int currentYear = DateTime.Now.Year;
if (copyrightDate < currentYear - 10 || copyrightDate > currentYear + 2)
throw new ArgumentOutOfRangeException($"The copyright year must be between {currentYear - 10} and {currentYear + 1}");
public override string ToString() => Title;
由于 Publication
类标记有 abstract
,因此无法直接通过以下代码进行实例化:
var publication = new Publication("Tiddlywinks for Experts", "Fun and Games",
PublicationType.Book);
不过,它的实例构造函数可以直接通过派生类构造函数进行调用,如 Book
类的源代码所示。
两个与出版物相关的属性
Title
是只读 String 属性,其值通过调用 Publication
构造函数提供。
Pages
是读写 Int32 属性,用于指明出版物的总页数。 值存储在 totalPages
私有字段中。 值必须为正数,否则会抛出 ArgumentOutOfRangeException。
与出版商相关的成员
两个只读属性:Publisher
和 Type
。 值最初是通过调用 Publication
类构造函数来提供。
与出版相关的成员
两个方法(Publish
和 GetPublicationDate
)用于设置并返回发布日期。 调用时,Publish
方法会将 published
标志设置为 true
,并将传递给它的日期作为自变量分配给 datePublished
私有字段。 如果 published
标志为 false
,GetPublicationDate
方法会返回字符串“NYP”;如果为 true
,则会返回 datePublished
字段的值。
与版权相关的成员
重写 ToString
方法
如果类型不重写 Object.ToString 方法,则返回类型的完全限定的名称,这对于区分实例没什么用。 Publication
类将 Object.ToString 重写为返回 Title
属性值。
下图展示了基类 Publication
及其隐式继承类 Object 之间的关系。
Book
类
Book
类表示作为一种特定类型出版物的书籍。 下面的示例展示了 Book
类的源代码。
using System;
public sealed class Book : Publication
public Book(string title, string author, string publisher) :
this(title, string.Empty, author, publisher)
public Book(string title, string isbn, string author, string publisher) : base(title, publisher, PublicationType.Book)
// isbn argument must be a 10- or 13-character numeric string without "-" characters.
// We could also determine whether the ISBN is valid by comparing its checksum digit
// with a computed checksum.
if (!string.IsNullOrEmpty(isbn))
// Determine if ISBN length is correct.
if (!(isbn.Length == 10 | isbn.Length == 13))
throw new ArgumentException("The ISBN must be a 10- or 13-character numeric string.");
if (!ulong.TryParse(isbn, out _))
throw new ArgumentException("The ISBN can consist of numeric characters only.");
ISBN = isbn;
Author = author;
public string ISBN { get; }
public string Author { get; }
public decimal Price { get; private set; }
// A three-digit ISO currency symbol.
public string? Currency { get; private set; }
// Returns the old price, and sets a new price.
public decimal SetPrice(decimal price, string currency)
if (price < 0)
throw new ArgumentOutOfRangeException(nameof(price), "The price cannot be negative.");
decimal oldValue = Price;
Price = price;
if (currency.Length != 3)
throw new ArgumentException("The ISO currency symbol is a 3-character string.");
Currency = currency;
return oldValue;
public override bool Equals(object? obj)
if (obj is not Book book)
return false;
return ISBN == book.ISBN;
public override int GetHashCode() => ISBN.GetHashCode();
public override string ToString() => $"{(string.IsNullOrEmpty(Author) ? "" : Author + ", ")}{Title}";
除了继承自 Publication
的成员之外,Book
类还定义了以下唯一成员和成员重写:
两个构造函数
两个 Book
构造函数共用三个常见参数。 其中两个参数(title 和 publisher)对应于 构造函数的相应参数。 第三个参数是 author,存储在不可变的 属性中。 其中一个构造函数包含存储在 自动属性中的 isbn 参数。
第一个构造函数使用 this 关键字来调用另一个构造函数。 构造函数链是常见的构造函数定义模式。 调用参数最多的构造函数时,由参数较少的构造函数提供默认值。
第二个构造函数使用 base 关键字,将标题和出版商名称传递给基类构造函数。 如果没有在源代码中显式调用基类构造函数,那么 C# 编译器会自动提供对基类的默认或无参数构造函数的调用。
只读 ISBN
属性,用于返回 Book
对象的国际标准书号,即 10 位或 13 位的专属编号。 ISBN 作为参数提供给 Book
构造函数之一。 ISBN 存储在私有支持字段中,由编译器自动生成。
只读 Author
属性。 作者姓名作为参数提供给两个 Book
构造函数,并存储在属性中。
两个与价格相关的只读属性(Price
和 Currency
)。 值作为自变量提供给调用的 SetPrice
方法。 Currency
属性是三位的 ISO 货币符号(例如,USD 表示美元)。 可以从 ISOCurrencySymbol 属性检索 ISO 货币符号。 这两个属性均为外部只读,但均可在 Book
类中由代码设置。
SetPrice
方法,用于设置 Price
和 Currency
属性的值。 这些值由那些相同属性返回。
重写 ToString
方法(继承自 Publication
)、Object.Equals(Object) 和 GetHashCode 方法(继承自 Object)。
除非重写,否则 Object.Equals(Object) 方法测试的是引用相等性。 也就是说,两个对象变量必须引用同一个对象,才算相等。 相比之下,在 Book
类中,两个 Book
对象必须包含相同的 ISBN,才算相等。
重写 Object.Equals(Object) 方法时,还必须重写 GetHashCode 方法,此方法返回运行时为了实现高效检索,在经哈希处理的集合中存储项所使用的值。 哈希代码应返回与测试相等性一致的值。 由于已将 Object.Equals(Object) 重写为在两个 Book
对象的 ISBN 属性相等时返回 true
,因此返回的哈希代码是通过调用 ISBN
属性返回的字符串的 GetHashCode 方法计算得出。
下图展示了 Book
类及其基类 Publication
之间的关系。
现在可以实例化 Book
对象,调用其唯一成员和继承的成员,并将其作为自变量传递给需要 Publication
或 Book
类型参数的方法,如以下示例所示。
public class ClassExample
public static void Main()
var book = new Book("The Tempest", "0971655819", "Shakespeare, William",
"Public Domain Press");
ShowPublicationInfo(book);
book.Publish(new DateTime(2016, 8, 18));
ShowPublicationInfo(book);
var book2 = new Book("The Tempest", "Classic Works Press", "Shakespeare, William");
Console.Write($"{book.Title} and {book2.Title} are the same publication: " +
$"{((Publication)book).Equals(book2)}");
public static void ShowPublicationInfo(Publication pub)
string pubDate = pub.GetPublicationDate();
Console.WriteLine($"{pub.Title}, " +
$"{(pubDate == "NYP" ? "Not Yet Published" : "published on " + pubDate):d} by {pub.Publisher}");
// The example displays the following output:
// The Tempest, Not Yet Published by Public Domain Press
// The Tempest, published on 8/18/2016 by Public Domain Press
// The Tempest and The Tempest are the same publication: False
设计抽象基类及其派生类
在上面的示例中定义了一个基类,它提供了许多方法的实现代码,以便派生类可以共用代码。 然而,在许多情况下,我们并不希望基类提供实现代码。 相反,基类是声明抽象方法的抽象类,用作定义每个派生类必须实现的成员的模板 。 通常情况下,在抽象基类中,每个派生类型的实现代码都是相应类型的专属代码。 尽管该类提供了出版物通用的功能的实现代码,但由于实例化 Publication
对象毫无意义,因此,使用 abstract 关键字来标记该类。
例如,每个封闭的二维几何形状都包含两个属性:面积(即形状的内部空间)和周长(或沿形状一周的长度)。 然而,这两个属性的计算方式完全取决于具体的形状。 例如,圆和正方形的周长计算公式就有所不同。 Shape
类是一个包含 abstract
方法的 abstract
类。 这表示派生类共享相同的功能,但这些派生类以不同的方式实现该功能。
以下示例定义了 Shape
抽象基类,此基类又定义了两个属性:Area
和 Perimeter
。 除了用 abstract 关键字标记类之外,还需要用 abstract 关键字标记每个实例成员。 在此示例中,Shape
还将 Object.ToString 方法重写为返回类型的名称,而不是其完全限定的名称。 基类还定义了两个静态成员(GetArea
和 GetPerimeter
),以便调用方可以轻松检索任何派生类实例的面积和周长。 将派生类实例传递给两个方法中的任意一个时,运行时调用的是派生类重写的方法。
public abstract class Shape
public abstract double Area { get; }
public abstract double Perimeter { get; }
public override string ToString() => GetType().Name;
public static double GetArea(Shape shape) => shape.Area;
public static double GetPerimeter(Shape shape) => shape.Perimeter;
然后可以从表示特定形状的 Shape
派生一些类。 以下示例定义了三个类:Square
、Rectangle
和 Circle
。 每个类都使用特定形状的专属公式来计算面积和周长。 一些派生类还定义所表示形状的专属属性(如 Rectangle.Diagonal
和 Circle.Diameter
)。
using System;
public class Square : Shape
public Square(double length)
Side = length;
public double Side { get; }
public override double Area => Math.Pow(Side, 2);
public override double Perimeter => Side * 4;
public double Diagonal => Math.Round(Math.Sqrt(2) * Side, 2);
public class Rectangle : Shape
public Rectangle(double length, double width)
Length = length;
Width = width;
public double Length { get; }
public double Width { get; }
public override double Area => Length * Width;
public override double Perimeter => 2 * Length + 2 * Width;
public bool IsSquare() => Length == Width;
public double Diagonal => Math.Round(Math.Sqrt(Math.Pow(Length, 2) + Math.Pow(Width, 2)), 2);
public class Circle : Shape
public Circle(double radius)
Radius = radius;
public override double Area => Math.Round(Math.PI * Math.Pow(Radius, 2), 2);
public override double Perimeter => Math.Round(Math.PI * 2 * Radius, 2);
// Define a circumference, since it's the more familiar term.
public double Circumference => Perimeter;
public double Radius { get; }
public double Diameter => Radius * 2;
以下示例使用派生自 Shape
的对象。 它实例化派生自 Shape
的一组对象,然后调用 Shape
类的静态方法,用于包装返回的 Shape
属性值。 运行时从派生类型的重写属性检索值。 以下示例还将数组中的每个 Shape
对象显式转换成其派生类型;如果显式转换成功,则检索 Shape
的特定子类的属性。
using System;
public class Example
public static void Main()
Shape[] shapes = { new Rectangle(10, 12), new Square(5),
new Circle(3) };
foreach (Shape shape in shapes)
Console.WriteLine($"{shape}: area, {Shape.GetArea(shape)}; " +
$"perimeter, {Shape.GetPerimeter(shape)}");
if (shape is Rectangle rect)
Console.WriteLine($" Is Square: {rect.IsSquare()}, Diagonal: {rect.Diagonal}");
continue;
if (shape is Square sq)
Console.WriteLine($" Diagonal: {sq.Diagonal}");
continue;
// The example displays the following output:
// Rectangle: area, 120; perimeter, 44
// Is Square: False, Diagonal: 15.62
// Square: area, 25; perimeter, 20
// Diagonal: 7.07
// Circle: area, 28.27; perimeter, 18.85