类
类的声明和实例化
class A{}
// another file
F(){
A a = new A(); // 实例化类,new <类名>(实例化参数)
A aa;
aa = new A(); // 先声明后实例化同样可行
}
类的成员
字段和方法
class A {
private string Name;
protected int Seq;
internal int Abc;
public int Score;
public void Foo(int x) {}
}
class B {
public static int Bef;
public static int Bar() {}
}
F(){
A a = new A(); // A中成员和方法都是非静态的,所以先创建一个A的实例
System.Console.WriteLine(a.score)
System.Console.WriteLine(B.b); // B中成员和方法都是静态的,所以通过类调用
}
属性
class A {
private string _Name; // 当综合使用private字段和public属性时
public string Name { // 用下划线加PascalCase为private字段命名,用PascalCase为public属性命名
get{ return _Name; }
set{ _Name = value; }
}
}
class B {
private string _Name;
public string Name{
get => _Name;
set => _Name = value; // 简化主体式表达方法
}
public int Seq { get; set; } // 使用自动实现的属性方法
}
在对属性使用或赋值时进行操作
class A {
private string _Name;
public string Name {
get => _Name;
set {
if (value == null)
throw new ArgumentNullException(nameof(value));
else if (value == "")
throw new ArgumentException("Value can't be empty", nameof(value));
else
_Name = value;
} // 对属性赋值进行检查
}
public int Seq {
get;
private set; // 把属性的方法声明为private就可以覆盖属性的public,这样只有本类才能使用set
// 注意:set只能比get更私有,若如果set为public,get为private则出错
}
public string Info { // 虚属性
get => $"{Seq} {Name}";
set {
string[] info = value.Split(new char[] { ' ' });
if (info.Length == 2 && int.TryParse(info[0], out int seq))
{ // not `out Seq` 属性不作out和ref的参数
Name = info[1];
Seq = seq;
}
else
throw new ArgumentException($"{value} is invalid");
}
}
public int Score {
get; // 只定义了get,则为只读属性
}
public bool pass {get; set; } = false; // 初始化属性
}
用自动实现的属性代替public,protected字段
注意不要出现如下代码
class A {
public string Name {
get; // Error
set => Name = value; // 错误:死循环
}
}
关键字this
class A {
private string Name;
private int Seq;
public void SetA(string name, int seq)
{
this.Name = name;
this.Seq = seq;
// this作为对自身的引用
}
}
没有必要不使用this
构造函数和解构函数
class A {
public string Name { get; set; }
public int Seq { get; set; } = -1;
public int Score { get; set; }
public bool Pass { get; set; }
public A(string name, int seq) { // 构造函数名和类名一样
Name = name;
Seq = seq;
}
public A(string name) => Name = name; // 重载构造函数,考虑为构造函数添加默认值而非简单重构
public A() :
this(string.Empty, -1) // 构造函数链,在C++中称为委托构造函数
{
Score = 59;
}
public void Deconstruct( // 解构函数,必须为void Deconstruct (out type typename) 形式
out string name,
out int seq,
out int score,
out bool pass
)
{
(name, seq, score, pass) = (Name, Seq, Score, Pass);
}
}
void Foo() {
A a = new A("Alice", 12); // 调用构造函数,我们给Seq的默认值-1会被覆盖
// new 获取空白内存,传递给构造函数进行实例化,再返回内存的引用
A aa = new A("Bob", 45) { Score = 60, Pass = true };
// 用初始化列表对可访问属性进行初始化
// 实际上是语法糖,与分别赋值等同
var aaa = new System.Collections.Generic.List<A>() {
new A("Ted", 78),
new A("Carol", 32)
}; // 集合初始化器
a.Deconstruct(out string name, out int seq, out int score, out bool pass); // 直接调用解构函数
var (name, _, score, pass) = a; // 元组语法
}
封装修饰符
封装
-
组合
面向对象编程可以将相关联的属性和方法结合在一个类中void Foo(int seq, string name) {} // 传递多个参数 void Foo(A a) {} // 传递一个类
相信先接触
struct
结构体类型的人应该更深地体会到将多个属性放在一个struct
中带来的方便之处,而class
不仅组合了属性,还有与之相关的多种方法。现在面向对象编程非常的成熟,众多高级语言都要使用到面向对象编程的思想。 -
隐藏
C语言中可以通过static
关键词使变量和函数仅在当前文件中可被访问,通过{int a}; /* can find a */
,作用域也是隐藏变量的手段之一。而在面向对象编程的实现中,一个class
就是一个单独的作用域,还有上述的private, public
等来控制成员在类外的可见性。
我们可以自由控制私有程度来隐藏类中的数据和实现细节,减少类的使用者对其的不恰当修改和破坏,只要提供给使用者一个接口就足够了。同时,只要接口不变,类的制作者进行修改后,使用者也不用修改自己的代码。
静态
静态字段、属性和方法属于类和全体实例
public class A {
public static int Count { get; private set; } // 计算A的实例化次数,静态字段会被默认初始化
public int Seq { get; set; }
A() => Seq = ++Count;
static A(/* 无参数 */) { // 静态构造函数,是对类的构造而非对实例的构造,在第一次使用类的时候被调用
Count = new System.Random().Next(0, 100); // 一般用于对静态成员的复杂初始化
} // 如果在此时抛出异常会使得A无法被构造而无法使用
}
public static class B { // 静态类
// 无法实例化
// 无实例字段和方法
// static类同时也是abstract和sealed,无法派生
// 可以使用using static引入静态类 using static System.Console
public static void Foo(this A a) => System.Console.Write(a.Seq);
// 在静态类中为其他类添加实例方法,在类型前加 this 关键字
// Foo的私有等级要比A高
static void F() {
a.Foo(); // A没有实例方法Foo,但我们在B中添加了
}
}
字段修饰符
class A {
const float Pi = 3.14159F; // const字段自动成为static字段
// const字段只能用于short, int, long, double等拥有字面值的数据类型
public readonly int Seq = 12; // 可以在类中指定初始值
public A (int seq) {
Seq = seq; // 只能在构造函数中更改
}
// readonly只用于字段(const可用于局部变量)
// readonly可以修饰非字面值的字段
private readonly System.Random rd = new System.Random();
// readonly -> read-only 由于只读属性的出现,readonly的使用大大减少了
}
访问权限修饰符
修饰符 | 本类 | 派生类 | 程序集 | 其他类 | 描述 |
---|---|---|---|---|---|
private | $$\surd$$ | ||||
protected | $$\surd$$ | $$\surd$$ | 派生类型 | ||
private protected | $$\surd$$ | $$\circ$$ | $$\circ$$ | 同程序集 且 派生类型 | |
internal | $$\surd$$ | $$\circ$$ | $$\surd$$ | $$\circ$$ | 同程序集 |
protected internal | $$\surd$$ | $$\surd$$ | $$\surd$$ | $$\circ$$ | 同程序集 或 派生类型 |
public | $$\surd$$ | $$\surd$$ | $$\surd$$ | $$\surd$$ |
派生类不能访问基类的
private
成员,除非派生类同时是基类的嵌套类
Assembly被翻译为“程序集”、“组合体”、“装配件”、“配件”等
类的修饰符只有 public
和 internal
,默认为 internal
修饰符主要用于成员
派生类访问基类的 protected
成员时,不能通过基类实例访问
class Base {
protected int N;
static protected int M;
}
class Derived: Base {
public void Foo() => System.Console.Write(N); // CORRECT
public void Bar() => System.Console.Write(Base.M); // CORRECT
public void Baz(Base b) => System.Console.Write(b.N); // ERROR
}
特殊类
嵌套类
class A {
private class B {
public Name;
public Seq;
private Aaa; // A无法访问private修饰的Aaa,但B可以访问A的所有成员
public void Foo(A a) { return a.N; } // 传递A的实例,B可以访问A的所有成员(无论private)
}
private int N = 54;
}
分布类和分部方法
// File: first.cs
partial class A {
partial void Foo(string s); //分布方法必须返回void,但可以用ref参数返回值
}
// File: second.cs
partial class A {
partial void Foo(string s) {
System.Console.WriteLine(s);
}
}
// 可用于将A中的嵌套类B分开
继承
派生
public class Base {
public string Name { get; set; }
}
public class Derived: Base {
public int Seq { get; set; }
}
void F() {
Derived d = new Derived();
System.Console.Write(d.Name);
// 每个派生类都有基类的所有成员,在继承链上也是如此
Baes b = d; // 派生类可以隐式地转化为基类
Derived dd = (Derived)b; // 基类必须显式地转化为派生类,但这有可能失败,确保你知道这么做的结果
// 注意:此时b是d的引用
b.Name = "abc";
System.Console.Write(d.Name); // display: abc
System.Console.Write(ReferenceEquals(a, b)); // True
// 事实上
Derived e = d;
System.Console.Write(ReferenceEquals(e, b)); // True
}
C#的单继承模式是其区别于C++的一个方面
转换
int i = 32;
double d = i; // 隐式
long l = 123;
i = (int)l; // 显式
class A {
public static explicit operator B(A a) { /* transform A into B */ } // 自定义显式转换
}
class B {
public static implicit operator A(B b) { /* transform B into A */ } // 自定义隐式转换
}
密封
public sealed class A { } // sealed使A无法被派生
public class Base {
public virtual void F() { }
}
public class Derived: Base {
public override sealed void F() { } // 使用sealed使该方法不能再被重写
}
string就是一个密封类
对比C++的
final
is操作符
string a = string.Empty;
System.Console.WriteLine(a is string); // True
System.Console.WriteLine(a is object); // True object是string的基类
System.Console.WriteLine(a is null); // False
object o = a;
System.Console.WriteLine(o is string); // True 装箱后依旧可以识别
System.Console.WriteLine(o is object); // True
class A { }
class B : A { }
B b = new B();
A a = b;
Write(a is B); // True
if (o is string) {
string s = (string)o; // 拆箱
// DoSometing
}
else if (o == null) {
// DoSomething
}
// 可以简写成如下形式
if (o is string s) {
// DoSometing
}
else if (o is null) {
// DoSomething
}
as操作符
class A { }
class B : A { }
void F(B b) { System.Console.Write(b == null ? "null" : "B"); }
A a;
F(a as B); // 类型转换,若失败,返回null
F((B)a); // Error
基类
虚函数
public class A {
protected int _N;
public virtual int N { // 虚属性和虚方法不能是private
get => _N;
set => _N = value;
}
}
public class B: A {
public override int N {
get => _N;
set => _N = value * 2;
}
}
public class C: A {
public new int N {
get => _N;
set => _N = value * 3;
}
}
public class D: B { // 继承B
public new int N {
get => _N;
set => _N = value * 4;
}
}
如果B不想使用A中N属性的定义,为了重写N属性,A显式使用 virtual
将一个函数修饰为虚函数,B使用现实 override
重写N定义,C使用 new
覆盖N定义(允许重写属性和方法不允许重写字段和静态成员,允许覆盖所有成员)
重写和覆盖
override
和 new
的区别
void F() {
B b = new B();
C c = new C();
D d = new D();
A a = b; // a是b的引用
a.N = 10;
System.Console.WriteLine(b.N); // display: 20
a = c; // a是c的引用
a.N = 10;
System.Console.WriteLine(c.N); // display: 10
a = d; // a是d的引用
a.N = 10;
System.Console.WriteLine(c.N); // display: 20
}
使用 override
调用的是重写链最末端的方法,使用 new
相当于截断了重写链
- A到B,末端为B,调用B的方法
- A到C,C处被截断,所以末端为A,调用A的方法
- A到D,D处被截断,所以末端为B(D继承B),调用B的方法
virtual
和 new
可以一起使用
void F() {
C c = new C();
B b = c;
b.F(); // display: 789
}
class A { public void F() => System.Console.Write("123"); }
class B : A { public virtual new void F() => System.Console.Write("456"); }
class C : B { public override void F() => System.Console.Write("789"); }
关键字base
class A {
public virtual string F() => "123";
public string G() => "789";
private int _N;
public int N { get => _N; set => _N = value; }
public A(int n) { _N = n; }
}
class B: A {
public override string F() => base.F() + base.G(); // 调用基类方法
public B(int n) : base(n) { } // 调用构造函数
}
抽象基类
抽象类对应具体类
public abstract class A {
public int N;
public abstract string GetS(); // 抽象成员(属性或方法)
}
public class B: A {
public override string GetS() => "123"; // 必须实现抽象成员
}
void F() {
A a = new A(); // 错误:抽象类无法被实例化
System.Console.Write(new B().GetS());
}
abstract
成员是自动virtual
的
对比C++的
= 0
来定义抽象函数:
- 可以在类外定义(可以有主体部分)
- 类不用再进行虚类声明
System.Object类
object
是所有类的基类,定义了如下方法
FuncName | Explanation |
---|---|
public virtual bool Equals(object o) | 参数和当前对象值相同与否 |
public virtual int GetHashCode() | 对象的Hash值 |
public Type GetTyep() | 对象对应类型的字符串 |
public static bool ReferenceEquals(object a, object b) | 是否表示同一个对象(地址) |
public virtual string ToString() | 对象实例的字符串表示 |
public virtual void Finalize() | 析构器的别名,无法直接调用 |
protected object MemberwiseClone() | 浅拷贝 |
接口
实现接口
接口是对类的实现的一种契约,实现接口的类必须实现接口所要求的属性和方法,而类的使用者只需要使用接口就可以了
创建接口
interface IFirst {
void Greet(string args);
string Name { get; set; }
// 接口中声明方法和属性(不能声明字段)
}
interface ISecond {
void Print();
int Seq { get; }
}
显式和隐式实现接口
class A: IFirst, ISecond { // 实现多个接口
// 实现主体
void IFrist.Greet(string args) {} // 显式实现必须加接口名,不加修饰符
public string Name { get; set; } // 隐式实现
// 隐式实现要求成员是public的
// 隐式实现的成员是virtual或sealed的(默认sealed)
// ISecond的实现
}
void F() {
A a = new A();
System.Console.Write(a.Name); // 隐式实现成员
((IFirst)a).Greet(string.Empty); // 显式调用必须进行强制转换
}
决定显式还是隐式:
- 成员是不是类的核心功能?隐式:显式
- 接口成员的名称是否恰当?隐式:显式
- 是否有同名成员?隐式:显式
抽象类实现接口
abstract class B: IFirst, ISecond {
public abstract void Greet(); // 抽象方法实现接口
public void Print() => System.Console.Write(123); // 不用abstract
// ...
}
接口扩展
接口和类的转换
void F(IFirst a) { /* */ } // 实现了IFirst接口的类的实例作参数
实现类型到接口的转换总是成功(隐式转换),但反之可能失败(需要显式转换)
接口继承
interface IFirst { void F(); }
interface ISecond: IFirst { void G(); }
class A: ISecond /* , IFirst */ {
void IFirst.F() /* not ISecond.F() */ { /* */ } // 显式实现需要用最初声明该方法的接口
void ISecond.G() { /* */ }
}
接口也支持多接口继承
当想要更改接口时,不要直接修改当前接口(会引起使用接口成员的函数和类失效),而是通过继承创建新的接口
接口的扩展方法
void F(this IFirst[] args) { /* */ }
// 用此方法实现“多继承”
static class M { public static void Foo(this I a) { System.Console.Write(12); } }
// 为接口实现扩展方法,这样每个实现类都有这个方法
interface I { }
class A: I { }
class B: I { }
void F() {
new A().Foo(); // display: 12
}
抽象类和接口的比较
抽象类 | 接口 |
---|---|
不能直接实例化,只能创建派生类 | 不能直接实例化,只能创建实现类 |
派生类要么也是抽象类,要么把所有抽象方法实现 | 实现类必须实现所有接口成员 |
可以添加非抽象成员 | 不直接添加,要通过继承来添加成员,否则会导致版本破坏 |
可以声明字段、属性和方法(包括构造函数和终结器) | 只能声明属性和方法 |
成员可以是 virtual abstract static 非抽象成员可以提供默认实现 |
成员都是默认 public sealed 的不能声明静态成员,所有成员都是自动抽象的 |
只能继承一个基类 | 可以多接口继承 |
抽象类是类,可以表示一种概念,是派生类的抽象本质(is something)
接口是契约,约定了类的实现和使用方法,是与外界交互的通道(can do something)
常用接口
名称 | 描述 |
---|---|
IComparable | 如果一个类要实现与其它对象的比较,则必须实现IComparable接口。 由可以排序的类型,例如值类型实现以创建适合排序等目的类型特定的比较方法。 |
IComparer | 是特定用于Array的Sort和BinarySearch方法, 通过实现IComparer接口的Compare方法以确定Sort如何进行对对象进行排序 |
IEnumerable | IEnumerable接口公开枚举数,该枚举数支持在集合上进行简单迭代。 IEnumerable接口可由支持迭代内容对象的类实现。 |
IEnumerator | IEnumerator接口支持在集合上进行简单迭代。是所有枚举数的基接口。 枚举数只允许读取集合中的数据,枚举数无法用于修改基础集合。 |
ICollection | ICollection接口定义所有集合的大小、枚举数和同步方法。 ICollection接口是System.Collections命名空间中类的基接口。 |
IDictionary | IDictionary接口是基于ICollection接口的更专用的接口。 IDictionary 实现是键/值对的集合,如Hashtable类。 |
IList | IList接口实现是可被排序且可按照索引访问其成员的值的集合,如ArrayList类。 |
NET Framework 2.0 以上版本的.net framework提供了响应泛型的接口,如IComparable
值类型
值类型和引用类型
特性 | 值类型 | 引用类型 |
---|---|---|
内存存储位置 | 存储在栈(stack)上 | 存储在堆(heap)上 |
拷贝策略 | 值类型变量之间复制时, 会拷贝出一个新的值(带 out ref 的值除外),更改其中一者不影响另一个 |
引用类型拷贝的是地址, 两者所引用的其实是同一个值, 改变其中一个会导致另一个所引用的值也改变 |
访问方式 | 直接包含数据 | 指向数据的指针 需要经过一次跳转才能访问到真正的变量 |
内存需求 | 较少 | 较多 |
执行效率 | 较快 | 较慢 |
内存回收 | 离开变量作用的定义域时 | 在所有引用消失后由垃圾回收器(GC)回收 |
声明和使用结构
public readonly struct M // 在struct前加readonly表明只读
{
// 不能声明M()默认构造函数
public M(int hours, int minutes, int seconds)
{
Hours = (hours + (minutes + seconds / 60) / 60) % 24;
Minutes = (minutes + seconds / 60) % 60;
Seconds = seconds % 60;
}
public M(int hours, int minutes) : this(hours, minutes, default(int)) { }
public M(int hours) : this(hours, default(int), default(int)) { }
public int Hours { get; } // 因为readonly所以不能实现set
public int Minutes { get; }
public int Seconds { get; }
public M Move(int hours, int minutes, int seconds) =>
new M(Hours + hours, Minutes + minutes, Seconds + seconds);
public override string ToString() =>
$"{Hours}:{Minutes}:{Seconds}";
} // 结构中的数据在使用new时调用构造函数显式初始化,使用数组时隐式初始化为默认值(default)
void F() {
M a = new M(600, 22, 45);
M b = new M();
}
建议在
struct
类型前加上readonly
修饰符使自定义值类型只读(就像内置的值类型)
对比C++的struct和class
继承和接口
除 enum
的值类型总是派生自 System.ValueType
, 而 System.ValueType
派生自 object
结构也能实现接口
如果希望比较相等性,应该重写 Equal(), ==, !=
等,并考虑实现 IEquatable<T>
接口
装箱和拆箱
object ob = new M(12);
System.Console.WriteLine($"{ob}"); // 重写了 ToString()
ob = "8465"; // 可存储不同类型
System.Console.WriteLine(ob);
M val = new M(12,45,78);
object obj = val; // boxing
val = (M)obj; // unboxing
值类型的装箱和拆箱需要注意
// 如果此时struct M是可变的
M m = new M(12,5,6);
object ob = m;
((M)ob).Hours += 5;
// 此时m的值没有改变
// 考虑如下相似情况
int val = 12;
ob = val; // ob装的是val
((int)ob) += 4; // 但是拆箱出的是被拷贝的一个临时值(根据值类型的定义),与val无关的值
// 事实上,上述代码是会报错的,因为int是readonly的
这就是我们为什么要将值类型声明为
readonly
的原因之一了
枚举
声明和使用枚举
enum State: short /* 指定除char外的基础类型,默认为int */ {
None,
On,
Off = 10, // 可以显式地为枚举赋值
Error, // ERROR = 11
Down = Error
}
void F() {
System.Console.Write(State.On);
System.Console.Write((int)State.Down);
}
类型转换
State a = (State)1; // 转换整型数字
State b = (State)Enum.Parse(typeof(State), "On"); // 转换string
if(Enum.TryParse("On", out c)) {
System.Console.Write(c);
}
FlagAttribute
如果枚举是可以组合的,如
[Flags]
enum State: short {
None = 0,
On = 1 << 0,
Off = 1 << 1,
Error = 1 << 2,
Down = Error
// State.Off | State.Down = 6
// 枚举值可以组合
}
则用 [Flags]
标记