C#
常量
1.任何使用常量的地方,编译器都会将这个常量替换为它的值;
2.使用const关键字声明,声明的同事必须使用具体的值来对其初始化;
public const string Message = “Hello World”;
3.常量比静态只读字段更严格:
常量在编译时就已经确定值;
4.值 改变 暴露 其他Assembley 静态制度字段 较好
YAssembly 引用 XAssembly 并使用其常量,编译时,常量值将固化在YAssembly内,后来,如果X重新编译,常量值改变,YAssembly未重新编译,则YAssembly使用未改变前的常量数值,只有YAssembly被重新编译后才会改变值.
静态构造函数:
1.静态构造函数,每个类型执行一次
2.非静态构造函数,每个实例执行一次
3.一个类型只能定义一个静态构造函数
必须无参
方法名与类型一致
在类型使用之前的一瞬间,编译器会自动调用类型的静态构造函数
实例化一个类型
访问类型的一个静态成员
只允许使用unsafe和extern修饰符
静态字段的初始化器在静态构造函数被调用之前的一瞬间运行
如果类型没有静态构造函数,那么静态字段初始化器在类型被使用之前的一瞬间执行,或者更早,在运行时突发奇想的时候执行
静态字段的初始化顺序与他们的声明顺序一致
类也可以时静态的,但是其成员必须全是静态的,不可以有子类
析构器
~Class1(){
}
Finalizer是class转悠的一种方法
在GC回收未引用对象的内存之前运行
其实就是对object的Finalize()方法重写的一种语法
局部类型
允许一个类型的定义分布在多个地方(文件)
nameof操作符
nameof操作符会返回任何符号(类型,成员,变量,)的名字
利于重构
继承
继承的类让你可以重用被继承类的功能
public class Asset{}
public class Stock : Asset{
}
子类可以引用父类的方法,父类不能引用子类的方法
多态
引用转换
一个对象的引用可以隐式的转换到其父类的引用(向上转换)
子类:Stack msft = new Stock();
Asset a = msft;
想转换的话到子类的引用则需要显示转换(向下转换)
引用转换:创建一个新的引用,它也指向同一个对象
As操作符
as操作符会执行向下转换,如果转换失败,不会抛出异常,值会变为null
Asset a = new Asset();
Stock s = a as Stock;
as操作符无法做自定义转换
long x = 3 as long;
is 操作符
冲操作符会检验引用的转换是否成功,换句话说,判断对象是否派生于某个类(或者实现了某个接口)
is操作符和模式变量
c#7,在使用is操作符的时候,可以引入一个变量
virtual函数成员
virtual函数可以被子类重写,包括方法,属性,索引器,事件
override重写
使用override修饰符,子类可以重写父类的函数
重写要求:
virtual方法和重写方法的签名,返回类型,可访问成都必须一样
重写方法里使用base关键字可以调用父类的实现
注意:
在构造函数里调用virtual
方法可能比较危险,因为编写子类的开发人员可能不知道他们在重写方法的时候,面对的是一个未完全初始化的对象.
换句话说,重写的方法可能会访问一类与还未被构造函数初始化的字段的属性或方法;
抽象类和抽象成员
使用abstract声明的类是抽象类
抽象类不可以被实例化,只有其具体的子类才可以实例化
抽象类可以定义抽象成员
抽象成员和virtual成员很想,但是不提供具体的实现.子类必须提供实现,除非子类也是抽象的
public abstract class Asset{
public abstract decimal NetValue{get;}
}
public override decimal NetValue => CurrentPrice * SharesOwned;
隐藏被继承的成员
父类和子类可以定义相同的成员:
public class A{public int Counter = 1;}
public class B : A {public int Counter = 2;}
calss B中的Counter字段就隐藏在了A里面的Counter字段(通常是偶然发生的).例如子类添加某个字段之后,父类也添加了相同的一个字段
如果是故意隐藏父类的成员,可以在子类的成员前面加上new修饰符
这里的new修饰符仅仅会抑制编译器的警告而已
public class A{public int Counter = 1;}
public class B : A {public new int Counter = 2;}
sealed密封
针对重写的成员,可以使用sealed关键字把它密封起来,防止它被其子类重写
public sealed override decimal Liability{get{return Mortgage;}}
也可以sealed类本身,就隐式的sealed所有的virtual函数了
base关键字
base关键字和this略像,base主要用于:
从子类访问父类里被重写的函数
调用父类的构造函数
public class House : Asset
{
public override decimal Liability => base.Liability + Mortgage;
}
这种写法可以保证,访问的一定是Asset的Liability属性,无论该属性是被重写还是被隐藏
构造函数和继承
子类必须声明自己的构造函数
从子类可访问父类的构造函数,的不是自动继承
子类必须重新定义它想要暴露的构造函数
隐式调用无参的父类构造函数
如果子类的构造函数里没有使用base关键字,那么父类的无参构造函数会被隐式的调用
public class BaseClass
{
public int X;
public BaseClass(){X = 1;}
}
public class Subclass : BaseClass
{
public Subclass(){Console.WriteLine(X);}//1
}
如果父类没有无参构造函数,那么子类就必须在构造函数里使用base
构造函数和字段初始化顺序
对象被实例化时,初始化动作按照如下顺序进行:
从子类到父类:
字段被初始化
父类构造函数的参数值被算出
从父类到子类
构造函数被执行
子类的字段先被初始化,其次是子类构造函数调用父类构造函数,再次是初始化父类的字段,然后是父类的构造函数,最后是执行子类构造函数的内容
重载和解析
static void Foo (Asset a) {}
static void Foo (House h){}
重载方法被调用时,更具体的类型拥有更高的优先级
House h = new House();
Foo(h);
调用哪个重载方法时在编译时就确定下来的
Asset a = new House();
Foo(a);
Object类型
所有类型的终极父类,所有类型均可以向上转换
Object
object是引用类型
但值类型可以转换未object,反之亦然.(类型同意)
stack.push();
int three (int)stack.Pop();
在值类型和object之间转化的时候,CLR必须执行一些特殊的工作,以弥合值类型和引用类型之间语义上的差异,这个过程叫做装箱和拆箱.
注意:
装箱对于类型统一是非常重要的,但是系统不够完美;
数组和反省只支持引用转换,不支持装箱
装箱会吧值类型的实例复制到一个新的对象
拆箱会吧这个对象的内容在复制给一个值类型的实例
静态和运行时类型检查
C#的程序既会做静态的类型检查(编译时),也会做运行时的类型检查(CLR)
静态检查:不运行程序的情况下,让编译器保证你程序的正确性
int x = “5”;
运行时的类型检查由CLR执行,发生在向下的引用转换或拆箱的时候.
运行时间差之所以可行,是因为:每个在heap上的对象内部都会存储一个类型token.这个token可以通过调GetType()方法来获取
GetType方法与Typeof操作符
所有c#的类型在运行时都是以System.Type的实例来展现的
两种方式可以获得System.Type对象:
在实例上调用GetType方法
在类型名上使用typeof操作符
GetType是在运行时被算出来的
typeof是在编译时被算出(静态) (当涉及到泛型类型参数时,它是由JIT编译器来解析的)
ToString()方法
ToString()方法会返回一个类型实例的默认文本表示
所有的内置类型都重写了该方法
可以在自定义的类型上重写ToString()方法
如果不重写该方法,那么就会返回该类型的名称
public class Panda
{
public string Name;
public override string ToString() =>Name;
}
Panda p = new Panda{Name = “Petey”};
Console.WriteLine§;
当你调用一个被重写的object成员的时候,例如在值类型上直接调用ToString()方法,这时候就不会发生装箱操作
但是如果你进行了转换,那么装箱操作就会发生
int x = 1;
string s1 = x.ToString();
object box x;
string s2 box.ToString();
Struct
struct和class差不多,但是有一些不同
struct是值类型,class是引用类型
struct不支持继承()除了隐式的继承了object,具体点就是System.valueType)
Struct的成员
class能有的成员,struct也可以有,但是以下几个不行:
无参构造函数
字段初始化器
析构
virtual或protected成员
Struct的构建
struct有一个无参的构造函数,但是你不能对其进行重写.它会对字段进行按位置归零的操作;
当你定义struct构造函数的时候,必须显示的未每个字段赋值.
不可以有字段初始化器.
访问修饰符
public:完全可访问
internal:当前assembly或朋友assembly可访问,非嵌套类的默认访问级别.
private:本类可访问;
protected,本类或其子类可访问.
protected internal,联合了protected和internal的访问级别
朋友assembly
通过添加System.Runtime.CompilerServices.InternalsVisibleTo这个Assembly的属性,并指定朋友Assembly的名字,就可以吧internal的成员暴露给朋友Assembly;
[assembly:InternalsVisibleTO(“Friend”)]
如果朋友Assembly有Strong name,那么就必须指定其完整的160字节的public key;
[assembly:InternalsVisibleTO(“StrongFriend,PublickKey = 0024f000048c”)]
类型限制成员的访问级别
class C{public void Foo(){}}
内层访问级别不能超出外层访问级别
访问修饰符的限制
当重写父类的函数时候,重写后的函数和被重写的函数的访问级别必须一致
有一个例外:当在其他Assembly重写protected internal的方法时,重写后的方法必须是proteced.
接口
接口与class类似,但是它只为其成员提供了规格,而没有提供具体的实现
接口的成员都是隐式抽象的
一个class或者一个struct可以实现多个接口
public interface Ienumerator{
bool MoveNext();
object Current {get;}
void Reset();
}
接口的实现
接口的成员都是隐式public的,不可以声明访问修饰符
实现接口对它的所有成员进行public的实现:
internal class Countdown : Ienumerator
{
int count = 11;
public bool MoveNext() => count – > 0;
public object Current => count;
public void Reset(){throw new NotSupportedException();}
}
对象与接口的转换
可以隐式的吧一个对象转换成它实现的接口:
Ienumerator e = new Countdown();
While(e.MoveNext())
Console.Write(e.Current);
虽然Countdown是一个internal的class,但是可以处通过把它的实例转化成Ienumeratir接口来公共的访问它的成员.
接口的扩展
接口可以继承其它接口
public interface IUndoable{void Undo();}
public interface Iredoable : IUndoable{void Redo();}
IRedoable继承了IUndoable的所有成员
显式的接口实现
实现多个接口的时候可能会造成成员签名的冲突.通过显式实现接口成员可以解决这个问题.
interface I1{void Foo();}
interface I1{int Foo();}
public class Widget : I1,I2
{
int I2.Foo()
{
Console.WriteLine();
return 42;
}
}
本例中,想要调用相应实现的接口方法,只能把其实例转换成相应的接口才行:
Widget w = new Widget();
w.Foo();
((I1)w).Foo();
((I2)w).Foo();
另一个显式实现接口成员的理由是故意隐藏那些对云类型来说不常用的成员.
virtual的实现接口成员
隐式实现的接口成员默认是sealed的
如果想要进行重写的话,必须在基类中把成员标记为virtual或者abstract.
public interface IUndoable{void Undo();}
publci class TextBox : IUndoable
{
public virtual void Undo() => Console.WriteLine(“TextBox.Undo”);
}
public class RichTextBix : TextBox
{
public override void Undo() => Console.WriteLine(“RichTextBix.Undo”);
}
显式实现的接口成员不可以被标记为virtual,也不可以通过寻常的方式来重写,但是可以对其进行重新实现.
在子类中重新实现接口
子类可以重新实现父类已经实现的接口成员
重新实现会"劫持"成员的实现(通过转换为接口,然后调用),无论在基类中该成员是否是viirtual的.无论该成员是否是virtual的.无论该成员是显式的还是隐式的实现(但最好还是显式实现的).
public interface IUndoable{void Undo();}
public class TextBox : IUndoable
{
void IUndoable.Undo() => Console.WriteLine(“TextBox.Undo”);
}
public class RichTextBox : TextBox,IUndoable
{
public void Undo() => Console.WriteLine(“RichTextBox.Undo”);
}
如果Textbox是隐式实现的Undo
public class TextBox : IUndoable
{
public void Undo() => Console.WriteLine(“TextBox.Undo”);
}
那么:
RichTextBox r = new RichTextBox();
r.Undo();
((IUndoable)r).Undo();
((TextBox)r).Undo();
说明重新实现接口这种劫持只对转化为接口后的调用起作用,对转化为基类后的调用不起作用.
重新实现适用于重写显式实现的接口成员.
重新实现接口的替代方案
即使是现实实现的接口,接口的重新实现也可能有一些问题:
子类无法调用积累的方法
基类的开发人员没有预见到方法会被重新实现,并且可能不允许潜在的后果
最好的办法是设计一个无需重新实现的基类:
隐式实现成员的时候,按需标记virtual
显式实现成员的时候,可以这样做:
public class TextBox : IUndoable
{
void IUndoable.Undo() => Undo();
protected virtual void Undo() => Console.WriteLine(“TextBox.Undo”);
}
public class RichTextBox : TextBox
{
protected override void Undo() => Console.WriteLine(“RichTextBox.Undo”);
}
如果不想有子类,那么直接把class给sealed.
接口与装箱
把struct转化为接口会导致装箱
调用struct上隐式实现的成员不会导致装箱
interface I{void Foo();}
struct s : I{public void Foo();}
S s = new S();
s.Foo();
I i = s;
i.Foo();
enum枚举
什么事枚举
枚举事一个特殊的值类型,它可以让你指定一组命名的数值常量.
public enum BOrderSide{Left,Right,Top,Bottom}
BorderSide TopSide = BorderSide.Top;
bool isTop = (topSide == BorderSide.Top);
枚举的底层原理
每个枚举都对应一个底层的整型数值(Enum.GetUnderlyingType()).默认:
是int类型
0,1,2会按照枚举成员的声明顺序自动赋值
也可以指定其他类型作为枚举的整数类型,例如byte:
public enum BorderSide : byte{Left,Right,Top,Bottom}
也可以单独指定枚举成员对应的整数值
public enum BorderSide : byte {Left = 1,Right = 2, Top = 10,Bottom = 11}
也可以只指定其中某些成员的数值,未被复制的成员将接着它前面已赋值成员的值递增
枚举的转换
枚举可以显式的和其底层的数值相互转换
int i = (int)BorderSide.Left
BorderSide side = (BorderSide)i;
bool leftOrRight = (int) side <= 2;
HorizontalAlignment h = (HorizontalAlignment) (int) BorderSide.Right;
0
在枚举表达式里,0数值会被编译器特殊对待,它不需要显式的转换:
BorderSide b = 0;
if(b == 0)
因为枚举的第一个成员通常被当做"默认值",它的值默认就是0;
组合枚举里,0表示没有标志(flags)
Flags enum
可以对枚举成员进行组合
为了避免歧义,枚举成员的需要显式的赋值,典型就是使用2的乘幂
[Flags]
public enum BorderSides{None = 0, Left = 1, Right = 2,Top = 4, Bottom = 8}
flags enum, 可以使用位操作符,例如 |和&
BorderSides leftRight = BOrderSides.Left| BorderSides.RIght;
if((leftRight & BorderSides.Left) != 0)
Console.WriteLine(“Includes Left”);
string formatted = leftRight.ToString();
BorderSides s = BOrderSides.Left;
s |= BorderSides.Right;
Console.writeLine(s == leftRight);
s^= BorderSides.Right;
Console.WriteLine(s);
Flags属性
按约定,如果枚举成员可组合的话,flags属性就应该应用在枚举类型上.
如果声明了这样的枚举却没有使用flags属性,你仍然可以组合枚举的成员,但是调用枚举实例的ToString()方法时,输出的将时一个数值而不是一组名称.
按约定,可组合枚举的名称应该是复数的.
在声明可组合枚举的时候,可以直接使用组合的枚举成员作为成员:
public enum BorderSides{
None = 0, Left = 1, Right = 2,Top = 4, Bottom = 8
LeftRight = Left | Right,TopBottom = Top | Bottom,All = LeftRight | TopBottom
}
枚举支持的操作符
= == != < > <= >= + - ^ & | ~ += -= ++ – sizeof
其中按位的,比较的,算数的操作符返回的都是处理低层值后得到的结果
假发操作符只允许一个枚举和一个整型数值相加,两个枚举相加是不可以的
嵌套类型
嵌套类型就是声明在另一个类型作用范围内的类型
public class TopLevel
{
public class Nested{}
public enum Color {Red, Blue, Tan}
}
嵌套类型特征
可访问封闭类型的私有成员,以及任何封闭类型能访问的东西
可以使用所有的访问修饰符来声明,不仅仅是public和internal
嵌套类型的默认访问级别是private而不是internal
从封闭类型外边访问嵌套类型需要使用到封闭类型的名称
public class TopLevel
{
public class Nested{}
public enum Color {Red,Blue,Tan}
}
ToLevel.Color color = TopLevel.Color.Red;
public class TopLevel
{
static int x;
class Nested
{
static void Foo(){Console.WriteLine (TopLevel.x)}
}
}