内容:
第三章: C#中创建类型
- 类
- 继承
- object类型
- struct
- 访问修饰符
- 接口
- 枚举
- 嵌套类
- 泛型
1. Classes
典型的class定义:
class YourClassName
{ }
除此之外:
位置 | 内容 |
---|---|
class前 | Attributes and class modifiers, 非嵌套类modifiers包括: public,internal, abstract, sealed, static, unsafe, partial |
类名之后 | 泛型类,基类,接口 |
大括号内 | 类成员,包括: methods, properties, indexers, events, fields, constructors,overloaded operators, nested types, and a finalizer |
1.1 字段
a.字段修饰符:
名称 | 修饰符 |
---|---|
Static modifier | static |
Access modifiers | public internal private protected |
Inheritance modifier | new |
Unsafe code modifier | unsafe |
Read-only modifier | readonly |
Threading modifier | volatile |
b.readonly修饰符
static readonly int legs = 8, eyes = 2; // 使用逗号定义多个
1.2 方法
a.方法修饰符:
名称 | 修饰符 |
---|---|
Static modifier | static |
Access modifiers | public internal private protected |
Inheritance modifier | new virtual abstract override sealed |
Partial method modifier | partial |
Unmanaged code modifiers | unsafe extern |
Asynchronous code modifier | async |
b.Expression-bodied method
当只包含一条语句的时候:
int Foo (int x) => x * 2;
void Foo (int x) => Console.WriteLine (x);
c.方法重载
函数签名:函数名以及它对应的参数类型顺序(参数名不算, params也不算, 但是ref/out算)
当函数签名不同的时候,就可以重载; 重载中,参数是值传递还是引用传递也可以重载,比如:
Foo(int)
可以和Foo(ref int)
或者Foo(out int)
共存,但是 Foo(ref int)
和Foo(out int)
不可以共存。
d.local method
局部 函数只能被嵌套的函数看到。局部函数可以出现在 setter, 构造器, lambda表达式中。
不能使用static
修饰局部方法,如果外部方法是static的,那么局部函数就会自动成为static
。
void WriteCubes()
{
Console.WriteLine (Cube (3));
Console.WriteLine (Cube (4));
Console.WriteLine (Cube (5));
int Cube (int value) => value * value * value;
}
1.3 构造器
class或者struct都有构造器。
a.构造器的修饰符:
名称 | 修饰符 |
---|---|
Access Modifiers | public internal private protected |
unmanged code modifiers | unsafe extern |
从C#7.0开始,构造器也可以使用 expression-bodied:
public Panda (string n) => name = n;
b.调用其他构造器
this
关键字和base
关键字。被调用的会先执行
public class Wine{
public Wine (decimal price) { Price = price; }
public Wine (decimal price, int year) : this (price) { Year = year; } //被调用的会先执行
}
传入构造器的参数可以是表达式,但是不能调用this
,因为此时instance自身都没有创建;不过没有使用static方法。
c.隐式无参public构造器
d.字段初始化顺序
字段的初始化是比构造器初始化更早的,并且按照他们在class中的定义顺序
e.非public构造器
1.4 Deconstructor(C# 7.0)
把一个对象的结果返回给多个数据。Deconstructor也可以重载
class Rectangle
{
public readonly float Width, Height;
public Rectangle (float width, float height)
{
Width = width;
Height = height;
}
///deconstructor
public void Deconstruct (out float width, out float height)
{
width = Width;
height = Height;
}
}
var rect = new Rectangle (3, 4);
(float width, float height) = rect; // Deconstruction
相当于:
float width, height;
rect.Deconstruct (out width, out height);
1.5 Object Initializers
Bunny b1 = new Bunny { Name="Bo", LikesCarrots=true, LikesHumans=false };
//相当于
Bunny temp1 = new Bunny(); // temp1 is a compiler-generated name
temp1.Name = "Bo";
temp1.LikesCarrots = true;
temp1.LikesHumans = false;
Bunny b1 = temp1;
编译产生了一个临时变量,这就保证初始化中如果出现错误,不会存在一个初始化了一半的对象。
1.6 this
1.7 Properties
a.修饰符
名称 | 修饰符 |
---|---|
Static modifier | static |
Access modifiers | public internal private protected |
Inheritance modifiers | new virtual abstract override sealed |
Unmanaged code modifiers | unsafe extern |
b.read-only property
c. expression-bodied properties
decimal currentPrice, sharesOwned;
public decimal Worth => currentPrice * sharesOwned; //get
public decimal Worth
{
get => currentPrice * sharesOwned;
set => sharesOwned = value / currentPrice;
}
d.自动属性
编译器自动会产生一个我们无法访问到的字段。
public class Stock
{
...
public decimal CurrentPrice { get; set; } //自动属性
}
e.初始化属性
public int Maximum { get; } = 999;
f.get和set的访问修饰符
g.CLR属性实现
CLR在compile的时候,把属性编译成get_XXX
and set_XXX
。 对于很简单的属性,JIT把它直接转化成了inline 语句,而不是方法,使其和直接访问filed的性能相当。
1.8 Indexers
索引也可以使用 ?[]
a.实现indexer
自己定义一个this
属性,后面加上[]
表示访问方式。indexer也可以有多个,只要访问方式不同。
class Person{
private string a;
public string this[int word]
{
get => "";
set => a = value;
}
public string this [int arg1, string arg2]
{
get { ... }
set { ... }
}
}
b.CLR实现indexer
clr将indexer编译成 get_item
和set_item
public string get_Item (int wordNum) {...}
public void set_Item (int wordNum, string value) {...}
1.9 Constants
常数是编译的时候就被替换了,可以是这些类型类型: built-in的numeric
,bool
,char
,enum
public const string Message = "Hello World";
const
比static readonly
更加严格,在编译的时候i就已经计算出结果然后进行替换了。只在绝对不会更改数据的情况下使用const
。
1.10 Static Constructor
一个类型只能定义一个静态构造器,且不包含参数。Runtime在这个类被使用之前自动调用静态构造方法,包括:
1. 初始化这个类型
2. 访问这个类型的静态成员
只有unsafe
和extern
修饰符能修饰静态构造器。
class Test
{
static Test() { Console.WriteLine ("Type Initialized"); }
}
a. 和属性初始化的顺序
先初始化静态成员,再初始化静态构造器。
class Foo
{
public static Foo Instance = new Foo(); //这里先调用构造器,再执行下一句
public static int X = 3;
Foo() { Console.WriteLine (X); } // 0
}
//如果把上面2个static 成员换位置,那么打印出来的就是3了
1.11 静态类
以static
修饰的类,只能由静态成员组成,且不能被继承。如System.Console
和System.Math
1.12 Finalizer
垃圾回收之前执行的, 实际上只override了object类的Finalizer方法。
class Class1
{
~Class1() //finalizer
{
...
}
}
1.13 Partial Class
每个定义的地方都要标记成partial
,他们之间不能有冲突的成员,可以标注父类,只要相同就行。
他们必须要在同一个assembly
中,编译的时候由编译器处理。但是编译器不保证字段的初始化顺序。
// PaymentFormGen.cs - auto-generated
partial class PaymentForm { ... }
// PaymentForm.cs - hand-authored
partial class PaymentForm { ... }
a. partial method
- 2个部分,一个方法定义,一个方法实现。
- 如果没有实现部分,则编译的时候这个方法自动会被去除
- partial method必须返回
void
,且本身是private
的
partial class PaymentForm // In auto-generated file
{
...
partial void ValidatePayment (decimal amount); //部分方法
}
partial class PaymentForm // In hand-authored file
{
...
partial void ValidatePayment (decimal amount) //实现方法的,当作hook使用
{
if (amount > 100)
...
}
}
1.14 nameof
string name = nameof (StringBuilder.Length); //返回Length
nameof (StringBuilder) + "." + nameof (StringBuilder.Length); //返回 StringBuilder.Length
2. Interitance
2.1 多态polymorphism
2.2 转换
//向上转
Stock msft = new Stock();
Asset a = msft; // Upcast
Console.WriteLine (a == msft); // True
//向下转
Stock msft = new Stock();
Asset a = msft; // Upcast
Stock s = (Stock)a; // Downcast
Console.WriteLine (s.SharesOwned); // <No error>
Console.WriteLine (s == a); // True
Console.WriteLine (s == msft); // True
a. as
运算符
向下转,转换失败返回null。 as操作符对数字类型转换以及自定义转换没用。
Stock s = a as Stock; // s is null; no exception thrown
b. is
操作符
判断一个对象是否是一个类的子类。 is操作符对数字类型转换以及自定义转换没用。
// pattern variable
if (a is Stock s)
Console.WriteLine (s.SharesOwned);
2.3 Virtual Function Members
方法、属性、indexer、event可以标记为virtual,而被子类重写。
public class Asset
{
public string Name;
public virtual decimal Liability => 0; // Expression-bodied property
}
public class House : Asset
{
public decimal Mortgage;
public override decimal Liability => Mortgage;
}
签名、返回值、访问修饰符都需要一样。
2.4 抽象类和抽象方法
抽象类无法被 初始化,只有它的实体子类可以。抽象类可以有abstract
子类成员。
2.5 Hiding Inherited Members
当基类和子类都有同名成员的时候,编译的时候会出警告。
public class A { public int Counter = 1; } //A.Account
public class B : A { public int Counter = 2; } //B.Account
使用new
修饰符,会明确的掩盖父类同名成员,让编译器不报警告:
public class A { public int Counter = 1; }
public class B : A { public new int Counter = 2; }
a. new和override的区别:
public class BaseClass
{
public virtual void Foo() { Console.WriteLine ("BaseClass.Foo"); }
}
public class Overrider : BaseClass
{
public override void Foo() { Console.WriteLine ("Overrider.Foo"); }
}
public class Hider : BaseClass
{
public new void Foo() { Console.WriteLine ("Hider.Foo"); }
}
Overrider over = new Overrider();
BaseClass b1 = over;
over.Foo(); // Overrider.Foo
b1.Foo(); // Overrider.Foo
Hider h = new Hider();
BaseClass b2 = h;
h.Foo(); // Hider.Foo
b2.Foo(); // BaseClass.Foo
2.6 Sealing function和class
当子类重写了父类的方法时,有时不希望这些方法继续被他们的子类重写,所以这时候使用sealed
来禁止重写效果。
public sealed override decimal Liability { get { return Mortgage; } }
也可以对这个类使用sealed
来封锁这个类中所有的重写结果。
2.7 base
关键字
base的作用:
- 在重写方法中访问被重写的父类方法(hidden的也适用)
- 调用父类的构造器
2.8 构造器的继承
子类可以访问父类的一些构造器,但是不是继承。
public Subclass (int x) : base (x) { } //父类的先调用
a. 自动调用父类无参构造器
如果子类构造器中没有使用base
,则会自动调用父类的无参构造器。
b. 对象初始化顺序
- 初始化自己的字段
- 计算调用的父类构造器的参数
- 父类初始化它的字段
- 父类执行构造器
- 子类执行自己的构造器
2.9 解决方法重构的调用
重构方法会先调用类型最匹配的那个,这个是编译时决定的。
//2个方法
static void Foo (Asset a) { }
static void Foo (House h) { }
House h = new House (...);
Foo(h); // Calls Foo(House)
Asset a = new House (...);
Foo(a); // Calls Foo(Asset)
3. The object type
当在值类型和object类型之间转换的时候,CLR要进行一些操作,来让这两种的用法不同。这叫做boxing
和unboxing
。
3.1 装箱拆箱
将值类型转换成引用类型叫装箱,引用类型可以是 object
或者接口
int x = 9;
object obj = x; // Box the int
int y = (int)obj; // Unbox the int
object obj = 9; // 9 is 被推断为int类型
long x = (long) obj; // InvalidCastException
object obj = 9;
long x = (int) obj; //成功
object obj = 3.5; // 3.5 is inferred to be of type double
int x = (int) (double) obj; // x is now 3
object[] a1 = new string[3]; // Legal,引用类型可以转换
object[] a2 = new int[3]; // Error 不能一起类型转换装箱
3.2 Static&Runtime Type checking
强制类型转换是在 Runtime,由CLR进行的,CLR通过获取 heap上对象的metadata来查看,比如说里面保存的 Type类型。
3.3 GetType方法和 typeof 操作符
所有C#类,都对应了一个System.Type
类型,获取方式:
- 对象.GetType() //运行时
- typeof 类型名 // 编译时
3.4 ToString()
注意,直接对值类型调用object的方法,不会发生boxing:
string s1 = x.ToString(); // Calling on nonboxed value
3.4 object对象的成员
public class Object
{
public Object();
public extern Type GetType();
public virtual bool Equals (object obj);
public static bool Equals (object objA, object objB);
public static bool ReferenceEquals (object objA, object objB);
public virtual int GetHashCode();
public virtual string ToString();
protected virtual void Finalize();
protected extern object MemberwiseClone();
}
4. Structs
和class的区别:
- 值类型
- 不支持继承
- 不能有无参构造器
- 不能有 字段初始化
- 不能有 finalizer
- 不能有 virtual或者protected的成员
4.1 构造器
- 默认有一个无法override的隐式无参构造器, 一直存在
- 当自己定义构造器的时候,必须初始化每个字段(无参构造器一直存在)
5. Access modifiers
修饰符 | 作用 |
---|---|
public | 公开访问; 是 enum 和interface 成员的默认修饰符 |
internal | 只对containing assembly or friend assemblies 公开,是非嵌套类的默认修饰符 |
private | 只对类内部可见,是 class 和struct 成员默认修饰符 |
protected | 对类内部和子类可见 |
protected internal | 上2个的组合 |
5.1 Friend Assembly
给internal
成员添加别的可以暴露的assembly
5.2 Accessibility Capping
类的访问修饰符,直接限制了成员可访问性的上限。
class C { public void Foo() {} } //C是internal,所以Foo虽然标注public,但实际只能是internal
5.3 Restrictions on Access Modifiers
当重写父类方法的时候,重写方法的可访问性只能 小于等于 原方法的可访问性
6. Interfaces
- interface的所有成员是
abstract
的, 需要被继承的 class或者struct来重写 - interface的成员都是public的,不能添加任何修饰符
- 实现类的成员,比如是internal的,那么将它转换成接口,还是能以public调用
public interface IEnumerator
{
bool MoveNext();
object Current { get; }
void Reset();
}
6.1 显式接口
使用接口名.方法名
来实现接口的方法。要调用这个方法,唯一的办法就是将其先转换成接口再调用
public class Widget : I1, I2
{
public void Foo()
{
Console.WriteLine ("Widget's implementation of I1.Foo");
}
//方法使用接口名.方法名
int I2.Foo()
{
Console.WriteLine ("Widget's implementation of I2.Foo");
return 42;
}
}
Widget w = new Widget();
w.Foo(); // Widget's implementation of I1.Foo
((I1)w).Foo(); // Widget's implementation of I1.Foo
((I2)w).Foo(); // Widget's implementation of I2.Foo
6.2 virtual实现接口
隐式实现的接口成员默认是sealed
,如果要他们能被重写,需要标注virtual
或者abstract
。
public interface IUndoable { void Undo(); }
public class TextBox : IUndoable
{
public virtual void Undo() => Console.WriteLine ("TextBox.Undo");
}
public class RichTextBox : TextBox
{
public override void Undo() => Console.WriteLine ("RichTextBox.Undo");
}
An explicitly implemented interface member cannot be marked virtual, nor can it be overridden in the usual manner. It can, however, be reimplemented.
6.3 子类中重新实现接口
子类可以重新实现一次接口,就算它被父类实现过一次了。
public interface IUndoable { void Undo(); }
public class TextBox : IUndoable
{
//这是 explicityly实现的
void IUndoable.Undo() => Console.WriteLine ("TextBox.Undo");
}
public class RichTextBox : TextBox, IUndoable //显式表明接口,再重新实现一次
{
public void Undo() => Console.WriteLine ("RichTextBox.Undo");
}
RichTextBox r = new RichTextBox();
r.Undo(); // RichTextBox.Undo Case 1
((IUndoable)r).Undo(); // RichTextBox.Undo Case 2
//当baseclass是隐式实现接口的时候,这有时就会导致一些没期望的结果
RichTextBox r = new RichTextBox();
r.Undo(); // RichTextBox.Undo Case 1
((IUndoable)r).Undo(); // RichTextBox.Undo Case 2
((TextBox)r).Undo(); // TextBox.Undo Case 3
这种重新实现接口的方式容易造成问题,特别对于父类来说,它可能并没有希望子类重新实现接口。如果真有这么个需求的话,那么要么父类使用 implicitly实现
,要么使用模板方法。
6.4 Interfaces and Boxing
把结构体转成interface
,会造成一次装箱。
7. Enums
public enum BorderSide { Left, Right, Top, Bottom }
一种特殊的值类型,默认底层是整形:
- 底层是int类型
- 默认赋值为0,1,2…
可以自己改变类型和值:
public enum BorderSide : byte { Left, Right, Top, Bottom }
public enum BorderSide : byte { Left=1, Right=2, Top=10, Bottom=11 } //没有指定的会依次增加
7.1 转换
对于enum来说,0是一个特殊值,不需要转换,默认代表定义的第一个值
int i = (int) BorderSide.Left; //转换成底层int
BorderSide side = (BorderSide) i; //从int转换
HorizontalAlignment h = (HorizontalAlignment) BorderSide.Right; //按照值相等,转换成另一个enum
HorizontalAlignment h = (HorizontalAlignment) (int) BorderSide.Right;
BorderSide b = 0; // No cast required
7.2 Flags Enum
可以组合Enum的值,为了避免歧义,成员一般都要赋值,且一般是2的指数:
[Flags]
public enum BorderSides { None=0, Left=1, Right=2, Top=4, Bottom=8 }
BorderSides leftRight = BorderSides.Left | BorderSides.Right;
// 定义的时候直接定义组合enum
[Flags]
public enum BorderSides
{
None=0,
Left=1, Right=2, Top=4, Bottom=8,
LeftRight = Left | Right,
TopBottom = Top | Bottom,
All = LeftRight | TopBottom
}
7.3 运算符
= == != < > <= >= + - ^ & | ˜
+= -= ++ -- sizeof
enum可以和整数加法,但是不能2个enum相加。
通常组合enum的Flag特性一定要定义;组合enum的名称一般是复数形式。
7.4 检查正确性
Enum.IsDefined //检查enum的值是否是正确定义的值中间的,但是对 组合enum 不起作用
8. Nested Types
特点:
- 可以访问包含类的所有成员,以及其他包含类能访问到的对象
- 可以使用所有modifier,不仅仅是public和internal
- 默认的可访问性是private
- 访问嵌套类,需要加上外部类的quanlified name
TopLevel.Color color = TopLevel.Color.Red;
9. Generics
好处: 增加代码重用,提高代码安全,减少转换和装箱
public class Stack<T>
{
int position;
T[] data = new T[100];
public void Push (T obj) => data[position++] = obj;
public T Pop() => data[--position];
}
在runtime,所有的 open type
都成为了 closed type
9.1 为什么存在Generics
9.2 Generic 方法
static void Swap<T> (ref T a, ref T b)
{
T temp = a;
a = b;
b = temp;
}
只有方法和类能够加入 type 参数,属性、indexer、events、字段、构造器等等,都只能利用已经引入的type 参数。
9.3 声明类型参数
classes, structs, interfaces, delegates能够在定义的时候,使用type parameter.
public struct Nullable<T>
{
public T Value { get; }
}
9.4 typeof 和 unbounded generic type
在runtime的时候,由于所有open type都已经替换成了close type, 所以在其之前,只有使用 typeof 能够获取 这些unbounded的Type类型,可以用于反射。
Type a1 = typeof (A<>); // Unbound type (notice no type arguments).
Type a2 = typeof (A<,>); // Use commas to indicate multiple type args.
9.5 默认generic value
使用default
可以帮助使用默认值。
9.6 Generic Constraints
where T : base-class // Base-class constraint
where T : interface // Interface constraint
where T : class // Reference-type constraint 引用类型
where T : struct // Value-type constraint (excludes Nullable types) 值类型
where T : new() // Parameterless constructor constraint
where U : T // Naked type constraint
class GenericClass<T,U> where T : SomeClass, Interface1
where U : new()
9.7 泛型子类
子类可以留着泛型参数, 也可以close, 也可以引入新泛型
class Stack<T> {...}
class SpecialStack<T> : Stack<T> {...}
class IntStack : Stack<int> {...}
class List<T> {...}
class KeyedList<T,TKey> : List<T> {...}
9.8 泛型引用自己
public class Balloon : IEquatable<Balloon>
9.9 静态数据
每一个closed type
都有自己的一份静态数据。
9.10 泛型中转换类型
使用as
或者先装箱再拆箱
9.11 Covariance Contravariance
从C# 4.0开始, 接口、deletegate、array都允许 covariant type parameter,但是class不行。
class Animal {}
class Bear : Animal {}
class Camel : Animal {}
public class Stack<T> // A simple Stack implementation
{
int position;
T[] data = new T[100];
public void Push (T obj) => data[position++] = obj;
public T Pop() => data[--position];
}
以下代码无法编译:
Stack<Bear> bears = new Stack<Bear>();
Stack<Animal> animals = bears; // Compile-time error
解释: 对于 bears来说,它本身内部是 Bears[] data = new Bear[100]
,如果上面语句成功了, 所以编译器担心会有 animals.Push (new Camel());
的情况发生,而Camel
无法放入 Bear[]
。
Covariance
Covariance : 假设A能转换成B,那么 X有一个 covariant type 参数如果 X<A> 也能转换成 X<B>
.使用out
,代表这个类型仅出现在 输出位置,即当输出Animal
的时候,输出Bear
也是可以的。
public interface IPoppable<out T> { T Pop(); } //out代表这个类型,仅用作输出位置
var bears = new Stack<Bear>();
IPoppable<Animal> animals = bears; // Legal
Contravariance
Contravariance : 假设A能转换成B,那么 X有一个 contravariant type 参数如果 X<B> 也能转换成 X<A>
以下in
代表这个类仅在输入位置,即输入 Bear
的时候,底层是Animal[]
接收,所以可以。
public interface IPushable<in T> { void Push (T obj); }
IPushable<Animal> animals = new Stack<Animal>();
IPushable<Bear> bears = animals; // Legal
bears.Push (new Bear());
9.12 C#泛型 vs. C++ templates
C# 中,closed type是在runtime确定的,所以在编译的时候,open type编译成了library (dll) ; 而C++来说,它在编译时就确定了这些类的源代码,这样也让运行时的动态检查特别麻烦。
C#中下面的例子无法编译,因为 +
并不能使用与所有类型
static T Max <T> (T a, T b)
=> (a > b ? a : b);
C++中,下面的代码对所有能使用+
的类型都编译了一遍
template <class T> T Max (T a, T b)
{
return a > b ? a : b;
}