C# 本质论2

【Note】局部变量名采用的是camel大小写形式,而且不包含下划线

【Note】隐式类型var
匿名类型的一个实例赋给一个隐式类型的变量:
var patent=
new { Title = "Bifocals",
YearOfPublication = "1784"};


【Note】可空修饰符
如 int? count = null;


【Note】默认情况下unchecked,即赋值溢出时采取截断,可以选择checked来引发异常

 


【Note】交错数组

声明一个交错数组: (内部数组都要实例化)
int[][] cells={
new int[]{1,0,2,0},
new int[]{1,2,0},
new int[]{1,2},
new int[]{1}
};

【Note】数组的Length属性问题

正常数组的Length是取得数组所有元素的总数
交错数组是取得内部包含的数组的数目
上面的cells.Length就等于4

int[,] array1 = new int[2, 3]; // 定义一个 2 X 3 的数组


int allLen = array1.Length; // 获取所有维度的元素数 6

int d1Len = array1.GetLength(0); // 获取第一个维度的元素数 2

int d2Len = array1.GetLength(1); // 获取第二个维度的元素数 3

【Note】一些数组的静态方法
string[] stringArray = new string[] {.....};
System.Array.Sort(stringArray ); //升序排序
System.Array.BinarySeach((stringArray ,"匹配内容") //该方法调用之前必须先Sort
System.Array.Reverse((stringArray) //将数组内的元素反序
System.Array.Clear(stringArray,0,stringArray.Lengh) //将数组内的元素置为默认值

【Note】数组的一些实例方法
Length
GetLength()
Rank //获取整个数组的维数
Clone()

【Note】数组的定义以及取值
int[,] xxx = new int[3,4];
int sss = xxx[0, 0];

int[][] fff = new int[3][]; //交错数组

*foreach遍历 交错数组是一个一个的数组 普通多维数组是一个一个的值

【Note】空接合运算符
例: fileName??"default.txt"

【Note】C#的命名空间必须显式导入,不能像java那样使用通配符 import javax.swing.*;
即C#不会导入任何嵌套的命名空间


【Note】函数参数
传入功能(传值) 传入传出功能(传引用ref) 传出(out)

【Note】参数数组
(1)要在方法声明的最后一个参数声明前面添加params,不能在非最后一个参数前面加
(2)最后个参数要声明为数组
实参可以逗号隔开,也可以以数组的形式

例: static void temp( params string[] par){} //声明方法
temp("sdfs", "sadasd"); //调用方法

【Note】函数声明的可选参数
例: static int function(string str1, string str2 = "str2Value")

* 默认值只能是常量


【Note】函数调用的命名参数
例: public void functions(string str1,string str2="str2Value,string str3="str3Value"") //函数声明
function(str1:"myValue" , str3:"myValue");//函数调用

 

【Note】属性支持为set和get添加访问修饰符
(该修饰符必须比该属性的修饰符 严格,比如属性是private,set和get就不能是public)

【Note】属性和方法调用不允许作为ref和out参数值使用


【Note】对象初始化器
Employee employee1= new Employee("Inigo","Montoya")
{Title="title" , Salary="Not enough"};


【Note】集合初始化器
List<Employee> employees = new List<Employee>()
{ new Employee("Inigo","Montoya"),
new Employee("Inigo","Montoya")
};

【Note】构造器初始化器

class myClass
{
public myClass(){ //其他代码 }

public myClass(string str):this(){ //其他代码 }

}

* 在构造函数内部是调用不了其他构造函数的,不像普通函数的重载

【Note】将匿名类型的实例赋给隐式类型的局部变量
var par = new { value1 = "par_value1", value2 = "par_value2" };
var par2 = new { par.value1, value2 = "par2_value2" };

System.Console.WriteLine(par2.value1);

最终输出par_value1

【Note】静态类
编译器会自动在CIL代码中将静态类标记为abstract和sealed。
* abstract说明不能实例化
* sealed说明不能被继承
* 仅包含静态成员
* 不能包含实例构造函数,但仍可声明静态构造函数
* 不能显式指定任何其他基类。
* 静态类不能实现接口(接口不能包含静态方法,而静态类不能包含实例方法)
* 静态类的成员不能有protected或protected internal访问保护修饰符。

【Note】扩展方法

1.定义一个静态类以包含扩展方法。该类必须对客户端代码可见。

2.将该扩展方法实现为静态方法,并使其至少具有与包含类相同的可见性。

3.该方法的第一个参数指定方法所操作的类型;该参数必须以 this 修饰符开头。

4.在调用代码中,添加一条 using 指令以指定包含扩展方法类的命名空间。

5.按照与调用类型上的实例方法一样的方式调用扩展方法。
(使用扩展方法最好的途径是通过继承来特化一个类型。如果扩展方法的签名跟类型已有方法的签名一样,那么扩展方法就会被覆盖。通过调用代码很难判断一个方法是不是扩展方法,如果对源代码没有控制权。问题就更加严重)

【Note】const
* const字段自动成为static字段
*const 字段只能在该字段的声明中初始化

【Note】readonly
* 与const不同,readonly只能用于字段,被声明的字段只能在构造器中修改,或者直接在声明时指定
* 与const不同,readonly字段可以是实例字段,也可以是静态字段,每个实例字段的值可以不同
*关键区别 const 字段为编译时常数,而 readonly 字段可用于运行时常数,如下例所示:
public static readonly uint timeStamp = (uint)DateTime.Now.Ticks;
*如果将readonly用于数组,只会冻结地址,而不会冻结内容

【Note】分部类
partial class className{} (原来的类也要加上partial)
*分部类不允许对编译好的类(其他程序集的类)进行扩展,只能运用与同一程序集

【Note】分部方法
在原来的类提供声明,在新增的类提供实现
* 分部方法只能是void,不能使用out,可以使用ref
*由于上面的规则,使得分部方法不必实现也不会产生任何影响

【Note】基类和派生类之间的转型
派生类可以隐式转换成基类,隐式转换不会为基类实例化一个新的实例,相反同一个实例会被引用为基类型


【Note】private
“派生类不能访问基类的private成员”
*假如派生类同时是基类的一个嵌套类,就不成立了

【Note】protect
基本规则:要从一个派生类中访问一个对象的受保护的成员,必须在编译的时候确定受保护的成员是派生类的一个实例
例: public class PadItem
{
protected string Name;


public class Contact: PadItem
{
void Load(PadItem padItem)
{
padItem.Name=... //这里出错
}

}

【Note】C#多重继承的一般解决方案————聚合
public class A {} //需要被继承的类


public class B //需要被继承的类
{
protect int first ;
}

public class C : A //C作为基类,直接继承A
{
private B BIntence { get ; set ;}

public int FIRST
{ get{return B.first} set{B.first=value}}
}


【Note】virtual
基类: virtual修饰方法
派生类: override 修饰方法

* virtual不能与static同时使用,因为当一个方法被声明为Static时,编译器会在编译时保留这个方法的实现(它属于类),而当一个方法被声明为Virtual时,直到你使用ClassName variable = new ClassName();声明一个类的实例之前,它都不存在于真实的内存空间中

* 不要在构造器中调用会影响所构造对象的任何虚方法,与C++不同(C++构造期间的virtual方法总是调用基类的实现),C#是根据设计原则:总是调用派生得最远的虚方法,即使构造函数尚未完全执行

 

【Note】C#规定只能使用下面这些限定符中的一个:
override virtual static abstract sealed
代表的含义分别为:
重载函数、虚拟函数、静态函数、抽象函数、密封函数(不可派生)


【Note】new实现的重载
在C#的角度看,new唯一的作用就是消除编译的警告
new跟override区别: new的重载没能实现多态


【Note】abstract类
次要特征:不可实例化
主要特征:类包含抽象成员
基类方法用abstract修饰
派生类用override修饰重载
*抽象方法是隐式的虚方法,其实现只能由派生类实现,而vertuial标记的虚方法可以由基类实现


【Note】is
public static void Save(object data)
{
if (data is string )
{
data=Encrypt((string)data); //加密数据
}
}

*在使用is之前,可以优先考虑多态性,比如在上面的例子中,从一个通用的基类派生,然后将这个基类类型作为Save()方法的一个参数调用,就可以避免显式地检查string

* as是强制转换类型,is是判断类型,假如一个对象object可以as成string,但他不一定is string判断的时候是true,as只能作用与引用类型,转换失败不引发异常

【Note】接口
* 区别:通过基类来共享成员签名,而接口只是共享成员签名
* 命名规范:采用Pascal大小写规范,第一个字母I开头
* 不包含实现,不包含字段,可以包含属性
* 所有成员不能有修饰符,CLI默认是public
* 接口不能包含static
* 接口不能显方用abstract修饰,因为CIL默认了abstract

 


【Note】接口的显式实现和隐式实现
* 显式实现不能添加修饰符,成员名称前面要添加接口名和一点作为前缀
* 显式实现的访问:实现接口的类的对象不能直接访问实现的方法,要先转换成接口对象,由接口对象来访问
* 隐式实现 修饰符只能是public,可选属性是virtual,如果没virtual,则该成员默认为sealed
* 总之,采用显式实现,接口方法就不作为实现接口的类的类成员,采用隐式实现,接口对象和实现接口的对象都能看见


【Note】接口设计的原则
(1)接口成员书不是核心的类功能?
如果接口方法单纯只是辅助方法,那就采用显式实现,若改接口方法是该类的主要功能,就采用隐式实现

(2)接口成员名作为类成员是否恰当?
名称是否产生歧义?总之一个接口成员在类中的用途不是很明确,就采用显式

(3)是否已经有一个同名的类成员?


【Note】接口的继承
interface IFirstface
{
int getNum();
}

interface ISecondface : IFirstface
{
int getNum2();
}

class Myclass : ISecondface
{
int IFirstface.getNum() //!!这里必须引用最初声明它的那个接口
{
return 1;
}

int ISecondface.getNum2()
{
return 2;
}

}

方法引用问题:
Myclass my = new Myclass();
ISecondface Is = my;
Is.getNum(); //!!派生的接口可以访问所有接口成员
Is.getNum2();

【Note】扩展方法在接口的应用
* 不要跟分部方法混淆了


【Note】通过接口来实现多重继承
下面例子是使用了 聚合和接口的方法:

class A{}

interface IB //IB确保B类和复制到C类的成员有
{ //一致的签名
string Str
{
get;
set;
}
}
class B:IB
{
public string Str
{
get { return "getStr";}
set { }
}

}

class C : A, IB
{

private B _B;

public string Str
{
get { return _B.Str; }
set { _B.Str = value; }
}
}

但是添加到B类的新成员不会同时添加到C类上,这还没有做到与“多重继承”同义

解决办法:
如果被实现的成员是方法(不是属性)的情况,
可以采用扩展方法对IB进行扩展,
这样凡是实现了IB的类都有了扩展的新方法

【Note】版本控制
* 扩展功能的办法 从一个原有的接口派生出一个新的接口


【Note】抽象类跟接口的比较
-----------------------------------------
抽象类:不能脱离它的派生类来实例化。抽象类构造器只能由它门的派生类调用
接口:不能实例化,不能有构造器
--------------------------------------------
抽象类:定义了实现类必须实现的抽象成员签名
接口:接口的所有成员要在实现类实现,不能只实现部分成员
-------------------------------------------
抽象类:扩展性比接口号,不会破坏任何版本的兼容性。在抽象类中,可以添加附加的非抽象成员,它们可以由所有派生类继承
接口:如果添加更多的成员扩展成员,会破坏版本兼容性
-----------------------------------------------
抽象类:可以包含存储在字段中的数据
接口:不能存储任何字段。只能在派生类中指定字段。解决办法就是在接口中定义属性,但不能包含实现
--------------------------------------------
抽象类:可以包含实现的virtual成员,所以能为派生类提供一个默认的实现
接口:所有成员自动成为virtual成员,而且不能包含任何实现
-----------------------------------------
抽象类:会占用之类唯一一个基类选项(单继承)
接口:最然不允许默认实现,但是实现接口的类可以继续相互派生

【Note】结构体
--------------------------
一个良好习惯:
应该确保值类型不变的。属性只有get而没有set。如果确实要修改,应该通过一个方法返回新的实例。 *思考为什么?*
-----------------------------------

* suruct跟类差不多,可以有字段,属性,方法,可以定义含参构造器,但是不能显式定义无参构造器(有时候根本不会调用构造器,比如定义数组的的时候,会直接采用默认值初始化,若可以自定义默认构造器,那么有时调用,有时不调用,所以初始化不统一了,因此C#禁止struct自定义默认构造器)
* 不能声明一个字段的同时进行初始化,原因同上
* 支持含参构造器,要求必须对所有字段初始化,否则编译报错
* 值类型都是密封的
* 值类型继承链:object->ValueType->struct
* 值类型还可以实现接口

【Note】必须要拆箱成为基础类型,例int 装箱成object,那只能拆箱成int,不能double之类的其他类型

【Note】lock语句不能用于值类型
假如用了值类型,那么就会装箱成堆的一个引用,与原来在栈中的引用对比总是不同的

【Note】说明值类型(struct)不可变的重要性
----------------------------------
IAngle是声明方法MoveTo的接口
Abgle是实现IAngle接口的类
其中MoveTo在Angle中改变其字段value的值,而不是返回一个新值的新实例
--------------------------------------

Angle angle = new Angle(25);
object objectAngle = angle;

((Angle)objectAngle).MoveTo(26);
输出:(Angle)objectAngle.value //25


((IAngle)angle).MoveTo(26)
输出:((Angle)angle).value // 25


((IAngle)objectAngle).MoveTo(26)
输出:((Angle)objectAngle).value //26

【Note】避免拆箱
* 拆箱指令不包括将数据复制回栈的动作
* 接口是引用类型,当通过接口访问已装箱的值时,拆箱和复制就可以避免

【Note】枚举之间的转换

MyEnum mm = MyEnum.A;
MyEnum2 mm2 = (MyEnum2) (int) mm;

----------
下面枚举数组的转换:
MyEnum[] mm= (MyEnum[])(Array) new MyEnum2[42];
这里利用了CLR的赋值兼容性比C#宽松的事实,
先转换成数组,在转换成第二个枚举,
前提是两个枚举具有相同的基础类型


【Note】枚举作为标记使用
[Flags]
enum MyEnum
{
A = 1<<0,
B= 1<<1
}
MyEnum mm = MyEnum.A | MyEnum.B;
Console.WriteLine(mm);
输出: A,B

如果没有 [Flags]特性,就会输出3

【Note】重写object的成员
* 重写ToString()
* 重写GetHashCode()
* 重写Equals(),必须同时重写GetHashCode()

【Note】重载运算符没看

【Note】类型封装
* 没有任何访问修饰符的类会被定义成internal(排除嵌套类,它默认是private)
* 命名空间中的类型声明可以只可以具有 public 或 internal 访问,默认internal
* 嵌套类可以使用public,internal,private,protected,protected internal
----------------------
嵌套类修饰符:
public:如果包容类是internal,则成员只在内部可见,如果是public,那么就可以从程序集外部访问

internal:成员只能从当前程序集访问

private:成员只能从包容类访问

protected:成员可以从包容类,以及派生的任何之类中访问,不管程序集是哪个

protected internal:成员可以从同一程序集的任何地方访问,并且可以从包容类型的任何派生类中访问,即使派生类不在同一程序集中


【Note】命名空间
可以嵌套定义:
namespace aaaa
{
namespace bbbb
{

}
}
也可以直接这样
namespace aaaa.bbbb
{

}
在CLR中是一样的

【Note】垃圾回收
* 在一些关键代码运行之前,可以先调用System.GC的Collect()方法,显著减少GC运行的可能性

* 弱引用(System.WeakReference)
private WeakReference Data;
public FileStream GetData()
{
FileStream data = (FileStream)Data.Target;
if (data != null)
{
return data;
}
else
{
//重新加载数据到弱引用Data
data = (FileStream)Data.Target;
return data;
}
}
上面代码要注意:
这里要先将弱引用Data的数据先赋给data先,
从而避免在"检查null"和"访问数据"两个动作之间,
垃圾回收器将弱引用清除


【Note】终接器
* 声明定义:~类名(){ }
* 终接器不负责回收内存,它主要职责是释放像数据库连接和文件句柄之类的资源

【Note】使用using进行确定性终结
* 终接器的调用是不确定性的,所以它只能作为后备机制
* 实现IDisposeable接口
public coid Dispose()
{
Close();
System.GC.SuppessFinalize(this);
//SuppessFinalize的作用是将this从终结队列(f-reachable)
//中移除,移除之后才能真正成为垃圾
}
* 使用using代码块,或者try/finally

【Note】资源利用和终结的指导原则
* 最好避免重写Finalize方法(总结方法),这会推迟垃圾回收,如果是数组,那么数组里面每一个对象都要执行一次终结方法
* 有终接器的对象应该实现IDisposeable来支持确定性终结
* 终接器应避免任何未处理的异常
* 像Dispose(),close()这样的方法应该调用System.GC.SuppessFinalize(this)使垃圾更快清理
* 资源清理方法应该足够简单,着重清理引用的资源,不要再引用其他对象
* 若基类实现了Dispose(),派生类应该调用基类的实现
* 调用Dispose()之后,对象不能再使用,出了调用Dispose(),Dispose()可以多次调用

【Note】延迟初始化
* 可以在属性的get里面判断为null的时候才进行成员初始化
*新方法:采用System.Lazy<T>

【Note】异常处理的指导原则
* 只捕捉你能处理的异常
* 不要隐藏你不能完全处理的异常
* 尽可能的少用Ststem.Exception
有些异常如OutOfMemoryException和StankOverflowException,这些异常不能捕捉到不处理,在CLR4中会采取关闭应用程序作为最佳操作,所以捕捉到应该保存易丢失数据,然后马上关闭程序,或者throw语句重新引发异常。
* 避免在调用栈较低的位置报告或记录异常(不记录异常的原因是 怕高级的栈重复引发异常导致重复记录)
* 在一个catch块中使用throw而不是throw <异常对象>(否则栈追踪不到原始位置)
* 重新引发不同的异常要小心
不仅会重置引发点,还会隐藏原始异常

【Note】模版接口使得类可以重复实现一个接口
class tempClass: IContainer<Address>,IContainer<Phone>
改进措施: 使用IContainer<object>

【Note】泛型类/接口的构造器
* 构造器跟普通类或者接口的构造器就行了,不能添加<T>,否则编译不能通过
* 在构造器赋值过程中,由于不知道T的具体类型,所以可以采用default()方法赋初值


【Note】Tuole 元组
* Tuple<int,string> temp= new Tuple<int,string>(20,"str");
* Tuple<int,string> temp= Tuple.Create(20,"str");
如果参数很多就用工厂方法create(),否则new一个元组太长了


【Note】接口约束

情景:在一个泛型类里面,因为要对T类型的对象排序,所以将对象转换成IComparable<T>对象,一般情况可以运作,但是如果传进来的T类型没有实现接口的话,必然出错。跟C++不同,C#不支持在类型参数上调用运算符(+,-,*等),它们是静态的,不能表现为接口或者基类约束的形式.

接口约束: class tempClass<T> where T:System.IComparable<T>

【Note】基类约束
* class tempClass<T> where T:基类名

【Note】struct/class约束
* class tempClass<T> where T:struct (这里约束T必须为值类型)
* struct/class约束不能与基类约束一起使用,因为那是没意义的


【Note】构造器约束
* class tempClass<T> where T:new() (这里约束T必须实现默认构造器)
* 不能约束含参构造器

【Note】含有约束的继承
* 约束可以由一个派生类进行继承,但是派生类必须显式地写出所有的约束,这样的设计是为了提高程序员使用派生类时的认知度,避免发现有约束时,却不知道约束从哪里来

* 相反情况:重写一个虚泛型方法,或者创建一个显式接口方法实现时,约束是隐式继承的,显式写出来反而编译出错

【Note】不允许的约束
* 泛型 不能提出对运算符的要求
比如方法签名(T first,T second),不能进行first+second运算
因为没有办法限制一个类必须有一个static方法,例如,接口不能指定static方法

* 泛型约束不支持OR条件
class tempClass<T> where T:IComparable<T>||Ifrmattable(这是错的)

* 不能约束 类型参数是委托或者枚举类型


【Note】泛型方法中的转型
有时候应该避免使用泛型
如下面代码:
public static T Deserialize<T>( Stream stream,Iformatter formatter)
{
return (T)formatter.Deserialize(stream);

}

执行formatter.Deserialize(stream)会返回一个object
在泛型方法里面执行转型,假如没有约束来验证转型的有效性,
那么级一定要非常小心了

但是真正调用的时候是下面情况:

string greeting=
xx.Deserialize<string>(stream,formatter)

上面代码看起来好像是强类型的
但实际上已经进行了隐式转换

如果改成:
string greeting=
(string)xx.Deserialize(stream,formatter)

这样应该更好描述代码所发生的事情


【Note】协变与逆变(主要应用与接口和委托)
参考文章:
http://www.cnblogs.com/artech/archive/2011/01/13/variance.html

例:
public delegate TResult Func<in T, out TResult>(T arg);

简单理解:
协变:父类引用子类
逆变:子类引用父类

在委托中,返回值用out 参数是属于输入值,用in
...

下面提起几个泛型协变和反变容易忽略的注意事项:

1. 仅有泛型接口和泛型委托支持对类型参数的可变性,泛型类或泛型方法是不支持的。
2. 值类型不参与协变或反变,IFoo<int>永远无法变成IFoo<object>,不管有无声明out。因为.NET泛型,每个值类型会生成专属的封闭构造类型,与引用类型版本不兼容。
3. 声明属性时要注意,可读写的属性会将类型同时用于参数和返回值。因此只有只读属性才允许使用out类型参数,只写属性能够使用in参数。

推理:
如果一个接口需要对T协变,那么这个接口所有方法的参数类型必须支持对T的反变
推理的例:
nterface IFoo<in T>
{

}

interface IBar<out T>
{
void Test(IFoo<T> foo);
}

如果一个接口需要对T进行协变或反变,那么这个接口所有方法的返回值类型必须支持对T同样方向的协变或反变


【Note】委托的声明
* C#编译器不允许定义一个直接或间接通过System.Delegate派生的(System.MuticastDelegate是System.Delegate的系统派生的子类)

* 使用delegate关键字声明一个委托数据类型
public delegate void MyFirstDelegateHander(int par1,string par2);

* 若委托是声明在一个类里面,那它便是一个嵌套类了

* 例如上面定义的MyFirstDelegateHander委托,是一个引用类型,但是不必使用new来实例化,直接传递函数名称,而不是实例化,这是C#2.0开始支持的委托推断


【Note】匿名方法与委托
* xxxHander xx= delegate(int first,int second){ return "asdasd"}
这里的delegate声明了一个委托字面量

*匿名方法允许省略参数列表,甚至委托类型发生了变化,只要返回类型与委托的返回类型兼容
delegate{ return "xxxx"}


【Note】系统定义的委托:Func<> Action<>(.net3.5开始存在)


【Note】Lambda
前提背景:delegate bool ComparisonHaander(int first,int second)

(int first,int second)=>{ return first<second}

还可以省略参数类型:
( first, second)=>{ return first<second }

无参数的情况:
Func<string> getUserInput=()=> {..return "input value"}

linq中:

表达式Lambda:
dx.tb_attachment.Where(item => item.a_id==1);

语句Lambda:
dx.tb_attachment.ToList<tb_attachment>().Where(item => { int a; return item.a_id == 1; });

【Note】Lambda表达式不是clr内部固有构造,它们的实现是由C#编译器编译时生成的
在clr中,匿名方法会被转换成一个单独的有编译器生成的静态方法

【Note】Lambda表达式使用外部变量(闭包)
如果匿名函数使用了外部变量,那么当这个委托执行的时候依然可以使用,这种现象称之为闭包(在clr中会定义一个闭包,外部变量和匿名函数都将定义成为这个闭包的实例成员)



C# 本质论2

上一篇:一次asp.net core3.1打造webapi开发框架的实践


下一篇:delphi7学习:文件构成