C#笔记 其一
类
类的声明和实例化
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 | |||||
protected | 派生类型 | ||||
private protected | 同程序集 且 派生类型 | ||||
internal | 同程序集 | ||||
protected internal | 同程序集 或 派生类型 | ||||
public |
派生类不能访问基类的
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]
标记
本文作者:violeshnv
本文链接:https://www.cnblogs.com/violeshnv/p/16831733.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步