C#学习中基础知识笔记
一、使用变量、操作符和表达式
1.在c#中,multiplicative操作符(*,/和%)的优先级高于additive操作符(+和-)。
2.结合性:操作符*和/都具有左结合性。
3.赋值操作符是从右到左结合的。最右侧的赋值最先发生,被赋的值从右向左,在各个变量之间传递。
eg. myInt5 = myInt4 = myInt3 = myInt2 = myInt = 10;
4.递增(++)递减(–)操作符的前缀和后缀:
count++:先返回,再++; ++count:先++,再返回;
count - -:先返回,再 - -; - -count:先 - - ,再返回;
5.变量赋值时赋给一个变量的值必须具有和变量相同的类型。此外,声明隐式类型的局部变量是使用var关键字,var关键字指示编译器根据用于初始化变量的表达式来推断变量的类型,且使变量值类型规定下来。
eg:
var myVariable = 99;
var myOtherVariable = "Hello";
其中myVariable、myOtherVariable是隐式类型变量,分别为int和string类型。
注意:只有在提供一个表达式来初始化变量的时候才可以使用关键字var。
二、方法和作用域
1.C#声明方法
returnType methodName(psrameterList)
{
//主体语句
}
注:returnType(返回类型)是一个类型名,eg:int,string。。。
methodName(方法名)所要调用的方法名称;
psrameterList(参数列表)描述允许传给方法的数据类型和名称,可以多个,要用“,”隔开。
eg:
int addValues(int leftHandSize,int rightHandSize)
{
//...
//这里添加方法主体语句
//...
}
注:必须显式指定任何一个参数的类型和整个方法的返回类型,不能用var;
returnType(返回类型)换成void来指明方法不返回任何值。
若返回类型不是void,则主体内部要写关键字return,再加表达式,且表达式类型要和方法指定类型相同。且return语句写在最后,return后的语句不执行。
eg:
int addValues(int leftHandSize,int rightHandSize)
{
//...
return leftHandSize + rightHandSize
}
void时,可以省略return语句,或者直接写个“return;”
2.作用域:
(1)定义局部作用域:
在方法主体中定义一个作用域,主体内声明的任何变量都具有方法的作用域。方法结束则随着消失。只能有方法内部的代码访问,称这种变量为局部变量。且局部变量在不同方法中无法共享。
eg:
calss Example
{
void firstMethod()
{
int myVar;
...
}
void anothorMethod()
{
myVar = 42; //错误,变量越界(不在当前方法的作用域中)
...
}
}
(2)定义类的作用域:
在类主体中定义一个作用域,主体内声明的任何变量都具有方法的作用域。,C#中使用字段描述有一个类定义的变量可以实现字段在不同的方法之间共享信息。
calss Example
{
void firstMethod()
{
myField = 42; //ok
...
}
void anothorMethod()
{
myField = 42; //ok
...
}
int myField = 0;
}
变量myField在类内部定义且在firstMethod和anothorMethod方法的外部,故具有类的作用域。且要注意:字段可以再类的任何位置定义,与方法中的变量先声明再使用不同。
三.使用决策语句
1.使用switch语句需要注意的:
(1)switch只能用于基本数据类型,例如int或string;对于其他类型例如float和double只能用if;
(2)case标签必须是常量表达式,如果在运行时计算case标签的值,则必须使用if;
(3)casse标签具有唯一性,不允许两个case具有相同的值;
(4)例子:
switch (trumps)
{
case Hearts:
case Diamonds: //允许直通——case标签之间无额外代码
color = "Red"; //针对Hearts和Diamonds这两种情况都会执行的代码
break;
case Clubs:
color = "Black";
case = Spades: //出错—case之间有额外代码且没有使用break跳出
color = "Black";
break;
}
(5)break语句是用来阻止直通的最常见的方式,但也可用一个return或throw语句来替代他。
四,使用复合赋值和循环语句
1.复合赋值操作符,eg:+=、%=、/=、-=、*=。
其中操作符+=除了运用于数字,也可用在字符串,eg:
string name = “STeve”;
string greeting = “Helllo”;
greeting += name;
Console.WriteLine(greeting); //输出的为“Hello STeve”
但只可+=可用在字符串上,其他操作符不行。
2.for语句中在“初始化”部分声明的新变量,其作用域值限制在for语句的主体中。一旦for语句结束,变量就会消失。
其次,由于每个变量只在各自的作用域内有效,所以多个for语句中可以使用相同的变量名。
五,管理错误和异常
1.假设:一个异常与try快末尾的多个catch处理程序匹配,会如何处理?
答:一个异常发生之后,将运行由“运行时”发现的第一个匹配的异常处理程序,其他处理程序会被忽略。eg,假设让一个处理程序捕捉Exception,后面又让一个处理程序捕捉FormatException,后者永不执行。因此,在一个try块之后,应该将较具体的catch处理程序放在较常规的catch处理程序之前。如果没有一个较具体的catch处理程序能够与异常匹配,就执行较常规的catch处理程序。
2.无论编译一个怎样的应用程序,都可以使用checked和unchecked关键字选择性打开和关闭程序的一个特定部分的整数溢出检查。
checked语句中的任何整数运算溢出都会抛出一个OverflowException异常:
int number=int.MaxValue;
checked
{
int willThrow=number++;
Console.WriteLine("永远都执行不到这里");
}
or:
int willThrow=checked(int.MaxValue+1);
unchecked则对于块中的整数运算都不会检查,就也不会抛出OverflowException异常:
int number=int.MaxValue;
unchecked
{
int wontThrow=number++;
Console.WriteLine("会执行不到这里");
}
or:
int wontThrow=unchecked(int.MaxValue+1);
但checked和unchecked关键字只能适用于int和long等整型执行的运算,不能来控制浮点运算。浮点运算永远不会抛出一OverflowException异常。
3.一个异常抛出后,它会改变程序的执行流程。这就意味着不能保证当一个语句结束之后,后面的语句肯定会运行。要注意,例如当一个语句的作用是释放它之前的一个语句获取的资源,那么就必须确保语句的执行。
4.可以使用finally块来保证某些语句的执行。Finally块要么紧接在try块之后,要么紧接在try块之后的的最后一个catch处理程序之后。只要程序进入与一个finally块相关联的try块,则finally块始终都会运行。
第二部分
创建并管理类和对象
1.一个类容纳的信息存储在字段中,类要提供的功能用方法来实现。
2.定义一个类时,程序只需要创建类的一个实例,然后调用类的方法。而使用一个类的程序不应该关心累的内部实际如何工作,这就是封装的中心思想。
封装的目的:(1)将方法和数据合并到一个类中,换言之,为了支持分类;
(2)控制对方法和数据的访问,换言之,为了控制类的使用。
3.类的主体中包含普通的方法和字段(也即变量)
4.关于方法和字段的命名:
public的标识符应该以大写字母开头,eg:public Area中的方法Area要以”A”开头;
非public的标识符(其中包括局部变量)应该以小写字母开头。
一个例外!:类名应该以大写字母开头,而构造器必须完全与类同名。所以一个private构造器也应该以大写字母开头。
5.构造器必须完全与类同名,但它没有返回类型(就连void都不能写)。构造器写法:与类同名的public/private方法(默认构造器的话不返回任何值)。
构造器的本质就是方法。
重载构造器后,生成应用程序时,编译器会根据为new操作符指定的参数来判断应该使用哪个构造器。 要注意的是:一旦为一个类写了任何构造器,编译器就不再自动生成默认构造器,如需用到,就必须手动写默认构造器。
1.理解null和可空类型:
null值常用于初始化引用类型,其本身就是一个引用。但不能把它复制给一个值类型,{eg: int i = null;// **非法!**
}
但是利用修饰符“?”,可以讲一个变量声明为一个可空值类型,可空值类型在行为上和普通值类型相似,但可以将一个null值赋给它。
{eg: int? i = null; // 合法
} 可以将值类型的常量赋给一个可空类型,但反之不行!
可空类型包含两个属性:HasValue 和 Value。其中HasValue属性指出一个可空类型是包含一个真正的值还是包含null。若包含一个真正的值就可以利用Value属性获取这个值。注意,Value属性是只读的,可读取但不能修改变量的值。
使用ref和out参数:
1.向方法传递一个实参时,对应的形参会用实参的一个副本初始化。换言之就是无论在方法内部进行什么修改,都不会影响作为参数来传递的一个变量的原始的值。
E.g:
static void DoIncrement(int param)
{
param++;
}
static void Main ( )
{
int arg = 42;
DoIncrement (arg);
Console.WriteLine (arg); //输出42,而不是43
}
在形参和实参附加ref关键字作为前缀,就可以使得参数成为实参的一个别名(或者对实参的一个引用),而不是实参的一个副本。从而上面输出的值就为43。即:
E.g:
static void DoIncrement(ref int param)
{
param++;
}
static void Main ( )
{
int arg = 42;
DoIncrement (ref arg);
Console.WriteLine (arg); //输出42,而不是43
}
“变量使用前必须赋值”这个规则同样适用于ref实参。
2.适用out参数,在形参附加out前缀而向方法传递一个out参数,必须在方法内部对变量进行赋值。所以调用方法时不需要对实参进行初始化。
E.g:
static void DoInitialize(out int param)
{
Param = 42; // 方法内部赋值
}
static void Main ( )
{
int arg; //未初始化
DoInitialize (out rag);
Console.WriteLine (arg); //输出42
}
栈和堆:
调用一个方法时,它的参数以及它的局部变量需要的内存总是从栈中获取,方法结束后自动归还给栈;
使用new关键字创建一个对象(类的一个实例),构造对象所需的内存总是从堆中获取,多个地方可以引用同一个对象,对象的最后一个引用消失后占用的内存呢可供重用。
(a.值类型在栈上创建、b.引用类型(对象)在堆上创建(引用本身还是在栈上)。即引用类型间接引用堆上的对象、c.可空类型实际为引用类型,在堆上)
第九章使用枚举和结构来创建值类型
1.C#支持两种值类型:枚举和结构。
2.结构和类的主要区别:
问题 结构 类
是值类型还是引用类型? 值类型 引用类型
它们的实例储存在栈还是堆? 结构的实例称为值,存储在栈上 类的实例称为对象,存储在堆上
可自己声明一个默认构造器? 不可以 可以
若声明自己的默认构造器,编译器仍会生成默认构造器吗? 会(自己写的构造器必须是非默认的构造器) 不会
在自己的默认构造器不初始化一个字段,编译器会帮初始化? 不会(会编译错误) 会
可在声明一个实例字段的同时初始化它吗? 不可以 可以
3.结构是值类型,复制值类型的变量会创建变量中的所有数据的一个副本。而类是引用类型,复制引用类型的变量时,复制的是对原始变量的一个引用,所以类变量中的数据发生改变,对这个变量的所有引用都会发生改变。(eg见p169.10)
第10章使用数组和集合
数组
1.数组是引用类型,不管其元素是什么类型。这就意味着数组变量只是引用了堆上的一个内存块,这个内存块容纳着实际的数组元素,而不是直接在栈上存储。
2.和类一样,声明一个数组变量时,不需要声明它的大小,只有在实际创建数组实例的时候才需要指定数组的大小(与类在用new关键字创建实例类似)。
3. 数组声明: int [ ] pins;
数组创建实例: pins = new int [4];
初始化数组变量: int [ ] pins = new int [4] { 9, 3, 7,2 };
(大括号中的值得数量必须和要创建的数组实例的大小完全匹配)
int [ ] pins = { 9, 3, 7, 2 };
复制数组:
Case1:单纯复制–数组是引用类型,单纯复制数组变量将获得同一个数组实例的两个引用;
Eg:
int [ ] pins = { 9, 3, 7, 2 };
Int [ ] alias = pins; //alias和pins引用同一个数组实例
Case2:真正复制数组实例–获得堆上实际数据的副本。Eg:
int [ ] pins = { 9, 3, 7, 2 };
int [ ] copy = new int [pins.Length];
For (int i = 0 ; i < pins.Length ; i++)
{
copy [i] = pins [i];
}
//上述代码等价于下列代码,CopyTo方法的使用
int [ ] pins = { 9, 3, 7, 2 };
int [ ] copy = new int [pins.Length];
pins.CopyTo(copy , 0);
//上述代码等价于下列代码,静态方法Copy的使用
int [ ] pins = { 9, 3, 7, 2 };
int [ ] copy = new int [pins.Length];
Array.Copy( pins, copy, copy.Length );
//④上述代码等价于下列代码,Clone方法的使用
int [ ] pins = { 9, 3, 7, 2 };
int [ ] copy = ( int [ ] ) pins.Clone ( );
多维数组:eg
int [ , ] items = new int [ 4, 6 ];
int [ , , ] cube = new int [ 5, 5, 5 ];
多维数组访问数组中的元素,要提供和维数相同的索引。
集合类
1.ArrayList集合类:(适合打散存储一个数组的元素)
ArrayList的Remove方法可以从ArrayList中移除一个元素,ArrayList将自动重新安排其元素的顺序;
ArrayList的Add方法可以在ArrayList的末尾添加一个元素;
ArrayList的Insert方法在ArrayList的中部插入一个元素;
④可以采取和普通数组一样的方式(通过方括号和元素索引)来引用ArrayList对象中的元素。
2.Queue集合类:实现先入先出。元素在队列的尾部插入,并从队列的头部移除。
3.Stack集合类:后入先出。元素在顶部入栈,也从顶部出栈。
4.Hashtable集合类:在内部维护两个object数组,一个容纳的是作为映射来源的Key(键),两一个容纳的是作为映射目标的value(值)。在一个Hashtable中插入一个Key/value对时,会自动跟踪key和value的映射,并允许你获取一个指定的key关联的value。
Hashtable不能包含重复的key;
使用foreach语句遍历一个Hashtable时,会返回一个DictionaryEntry。DictionaryEntry类允许通过key属性和value属性访问两个object数组的key和value元素。
5.SortedList 集合类: 与Hasftable主要的区别是在SortedList中keys数组总是排好序的。在SortedList中插入一个key/value对时,key会插入keys数组的一个正确的索引位置,目的是确保keys数组始终处于排好序的状态。
6.集合初始化器:某些集合类可以在声明的同时初始化。
Eg:ArrayList numbers = new ArrayList ( ) { 10, 9,8,7,6,5,4,3,2,1}
在内部,C#编译器实际会将这个初始化的过程转化为一系列对Add方法的调用。换言之只有支持Add方法的集合,才可以使用这个写法(Stack和Queue不支持Add方法)
比较数组和集合:
数组要声明它所容纳的元素的类型,集合则不声明。这是由于集合是以object形式来存储其元素的。
一个数组有固定的大小,不能增大或者减小。集合则可以根据需要动态改变大小。
数组可以是多维的,集合则是线性的。然而,集合中的项可以是集合自身,所以可以用集合来模拟多维数组。
第11章 参数数组
1.参数数组:用params关键字声明的数组参数。其作用是写一个方法就能接受数量可变的参数。Eg:假定要想控制台写入很多值,常规做法是要写很多Console.WriteLine版本来获取不同数量的参数,而使用params就可以简化这一过程
2.声明params数组: params int [ ] paramList;
3.关于params数组要注意:
(1)只能为一维数组使用params关键字,不能为多维数组使用;
Eg: //编译时错误
public static int Min(params int [ , ] table)
(2)不能只依赖params关键字来重载一个方法。Params关键字不构成方法的签名的一部分;
Eg: //编译时错误
public static int Min(int [ ] paramList)
public static int Min (params int [ ] paramList)
(3)不允许为params数组指定ref或out修饰符;
(4)Params数组必须是方法的最后一个参数。这表明每个方法只能有一个params数组参数;
Eg: //编译时错误
public static int Min(params int [ ] paramList , int i)
(5)非params方法总是优先于一个params方法,即:若愿意,仍然可以创建一个方法的重载版本,让它应用于更常规的情况:
Eg:
public static int Min(int leftHandSide ,int rightHandSide)
public static int Min(params int [ ] paramsList)
调用Min时,若传递两个int参数值,就使用Min的第一个版本;
若传递其他任意的int参数值(其中包括无任何参数值的情况),就使用第二个版本。
(6)编译器会检测并拒绝任何有可能有异议的重载。
Eg: //编译时错误
public static int Min(params int [ ] paramsList);
public static int Min(int, params int [ ] paramsList);
4.使用params object [ ] (Black.Hole方法):
当参数的数量、类型不固定时,可以使用一个object类型(所有类的根,涉及装箱)的参数数组来声明一个方法,它能接受任意数量的object参数值,换言之,不仅参数值的数量是任意的,参数的类型也可以是任意的。
Eg:
class Black
{
public static void Hole(params object [ ] paramsList)
}
(1)可以不向它传递任何实参。这种情况下编译器传递一个长度为0的object的数组:
Black.Hole ( );
//转换成Block.Hole(new object [0] );
(2)可以在调用 Block.Hole时,使用null作为实参。数组是引用类型,所以允许使用null来初始化一个数组:Block.Hole(null);
(3)可以向Block.Hole方法传递一个实际的数组。也就是说可以手动创建本应由编译器创建的数组:
object [ ] array = new object [2];
array[0] = “forty two”;
array[1] = 42;
Block.Hole(array);
(4)可以向Block.Hole传递不同类型的其他任何实参,这些实参自动包装到一个object数组中:
Block.Hole(“forty two”, 42 );
//转换成Block.Hole(new objtce [ ] {“forty two ”,42} )
第12章 使用继承
1.继承问题就是分类问题–继承反映了类与类之间的关系。
涉及共性:对于不同的类,但它们具有某种相同的共性,考虑代码描述共性和日后维护,可以用类的继承来解决这一问题。Eg:马、鲸鱼、人等都属于哺乳动物,为了描述这一共性,可以创建一个名为Mammal的类,用它对所以的哺乳动物的共性建模,然后分别声明House、Whale、Humman等类都继承于Mammal类。这样继承的类将自动拥有Mammal类的所有特性,再在此基础上对House、Whale、Humman等类声明各自的特性。
2.使用继承:声明一个类是从另一个类继承的
class DerivedClass : BaseClass {
.....
}
DerivedClass(派生类)将从BaseClass(基类)继承,基类中的方法会成为派生类的一部分。
3.一个类最多从一个其他的类派生。
4.在声明方法时,当派生类和基类声明一个相同名字的方法时。派生类的方法会屏蔽(或隐藏)基类中的具有相同名字的方法,同时编译器会发出一条警告消息。
此时代码还是能够编译并运行,可以将new关键字写在派生类的方法声明前面(new public void ~)消除警告,但其作用也就只有关闭警告。事实上,new关键字的作用就是说:“我知道我在干什么,不用警告我!”。但冲突没有消除。
5.在基类中将一个方法声明为virtual(虚)方法,派生类就可以使用override(重写)关键字来声明该方法的另一个的实现。在使用virtual和override关键字声明多态的方法时必须遵守:
(1)不允许使用virtual或者voerride关键字声明一个private方法;
(2)两个方法必须具有相同的名字、类型、参数数量以及返回类型;
(3)两个方法必须具有相同的可访问性。例如其中一个方法时public的,那么另一个也必须是public的(方法也可以是proteted的);
(4)只能override(重写)一个virtual方法。假如基类的方法不是virtual的,但你试图override它,就会得到一个编译时错误。这个设计是合理的:应该由基类的设计者决定一个方法能否override(重写);
(5)假如派生类不用virtual关键字来声明方法,就不会重写基类方法。会因重名而发出警告(可使用new关键字消除警告);
(6)?一个override方法将隐式地成为virtual方法,本身可在未来的一个派生类中被override。然而不允许使用virtual关键字将一个override方法显示地声明为virtual方法。
6.protected访问:使用protected关键字来标记成员,对于基类B来说:若类A是派生类,那么就能访问B的protected成员,也就是说在派生类A中,B的一个protected成员实际是public的;若类A不是类B派生的,就不能访问B的protected成员,也就是说在A中,B的protected成员实际是private的。
不仅能在派生类中访问一个protected基类成员,还能在派生类的派生类中访问。Protected基类成员在一个派生类中保持它的protected可访问性,并可在更深层的派生类中访问。
7.扩展方法:扩展方法允许通过附加的静态方法来扩展现有的类型(无论是类还是结构)。任何语句一旦引用被扩展类型的数据,就能立即开始使用这些静态方法。
扩展方法是在一个静态类中定义的,要扩展的类型必须是方法的第一个参数,且必须附加一个this关键字。Eg:(为int类型实现一个名为Negate的扩展方法)
static class Util
{
public static int Negate(this int i)
{
Return -i ; //求一个整数的相反数,一元求反操作符(-)
}
}
Negate方法的参数附加this关键字作为作为前缀表明这是一个扩展方法,this修饰的是int,意味着要扩展的是int类型。
使用扩展方法:只需让Util类进入作用域(如有必要添加一个using语句指定Util类所在的扩展命名空间),然后就可以简单使用“.”来引用方法。Eg:
int x = 591;
Console.WriteLine(“x.Negate {0}”, x.Negate( ) );
第13章 创建接口和定义抽象类
1.定义接口:使用interface关键字,在接口中要按照在类或者结构中一样的方式声明方法,只是不允许指定任何访问修饰符(不能限制public、private或protected访问)。另,还要将方法主体替换成一个分号。Eg:
Interface IComparable
{
int CompareTo (object obj) ; //CompareTo方法的声明
}
Btw:建议接口名以大写I开头
2.实现接口:要实现一个接口,需要声明一个类或结构,让它们从接口继承,并实现接口指定的全部方法。(同从基类继承有什么区别?)
实现一个接口,必须保证每个方法都完全匹配与它对应的接口中的方法,需遵循:
(1)方法名和返回类型完全匹配;
(2)所有参数(包括ref和out关键字修饰符);
(3)显示接口实现:使用接口名作为方法名的前缀;
(4)用于实现一个接口的所有方法都必须具有public可访问性,但使用显示接口实现就不应该为方法添加访问修饰符。
3.一个类可以在扩展另一个类的同时实现一个接口,先写基类名,再写一个逗号后写接口名。Eg:
class House : Mammal , ILandBound //Mammal基类,ILandBound接口名
{
......
}
4.一个类最多只能有一个基类,但可以是吸纳数量无限的接口。类必须实现它从它所有接口继承的所有方法。(先写基类,后写接口,逗号隔开) eg:
class House : Mammal , ILanfBound , IGrazable //基类Mammal;
{ //接口ILanfBound , IGrazable
......
}
5.接口的限制:接口永远不包含任何实现。
(1)不允许在接口中定义任何字段,即便是静态字段;
(2)不允许在接口中定义任何构造器;
(3)不允许在接口中定义任何解析器;
(4)不允许为任何方法指定访问修饰符。接口中所有方法都隐式地为public方法;
(5)不允许在接口中嵌套任何类型(例如枚举、结构、类或接口);
(6)虽然一个接口能从另一个接口继承,但不允许从结构或类中继承一个接口。结构和类都含有实现,假如允许接口从它们继承,就会继承一些实现。
6.抽象类:<前提>{将通用的实现放到专门为此目的而创建的一个新类中,以避免不同的类对某一方法实现而产生重复代码(是一个警告信息)}为了明确声明不允许创建某个类的实例,必须将那个显示地声明为抽象类。
定义:使用abstract关键字 eg:
**abstract** class GrazingMammal : Mammal , IGrazable
{ //GrazingMammal是指草食性哺乳动物,IGrazable食草
......
}
若试图实例化一个GrazingMammal 对象,代码无法通过编译。Eg:
GrazingMammal myGrazingMammal = new GrazingMammal (...); //非法
抽象类可以包含抽象方法,起原则上与virtual类似,但它不含方法主体,派生类必须重写(override)这个方法。
抽象方法适合在以下情形下使用:一个方法如果在抽象类中提供一个默认实现是没有意义的,但是,又要求继承类必须提供它自己对该方法的实现。 Eg:
abstract class GrazingMammal:Mammal, IGrazable
{
abstract void DigestGrass ( ) ;
}
7.密封类:不想一个类作为基类使用,可以使用sealed(密封)关键字防止一个类被用作基类。Eg:
Sealed class House : GrazingMammal, ILandBound
{
.....
}
在密封类中不能声明任何virtual方法,另抽象类不能密封。
也可以使用sealed关键字将未密封的类中的一个方法声明为密封方法。派生类不能重写(override)密封方法。
关键字小结:
-interface引入一个方法的名称;
-virtual方法是方法的第一个实现;
-override方法是方法的另一个实现;
-sealed方法是方法的最后一个实现;
第14章 使用垃圾回收和资源管理
1.对象创建的过程:(使用new关键字创建)
(1)首先,new关键字必须从heap(堆)中分配一些原始内存(无法干预);
(2)然后,new操作必须将那些原始数据内存转换成一个对象,它必须初始化对象。(在构造器的帮助下可以进行控制)。
创建类的实例:Square mySquaren=new Square( ); //Square是一个引用实例;
使用点操作符访问成员:mySquare.Draw ( ) ; //Square类包含Draw方法
其他引用变量引用同一个对象:Square referenceToMySquare = mySquare;
(引用次数不限。对同一个对象的引用决定对象的生存期,因为只有将全部的引用消失后才可以销毁这个对象。)
对象销毁的过程:
(1)首先,对应于创建对象的第二步,“运行时”必须执行一些清理工作。在一个析构器的帮助下可以对这一阶段进行控制;
(2)然后,“运行时”必须将之前被对象那个占有的内存归还给堆(heap),对象的内存必须回收,这一阶段没有控制权。
2.析构器:可以执行对象被垃圾回收是需要执行的任何清理工作。析构器也是一个特殊的方法,只是“运行时”会在一个对象的最后一个引用消失之后调用它。
析构器语法:“~”+ 类名;
只有当对象进行垃圾回收的时候才会运行析构器。析构器最终肯定会执行,只是不能保证在什么时候运行。因此,在写代码的时候不要对顺序或者垃圾回收的时间
析构器的限制:
(1)析构器只适用于引用类型,不能在值类型(eg:struct)中声明析构器;
Eg:
struct Tally
{
~Tally ( ) {...} //编译时错误
}
(2)不能为析构器指定一个访问修饰符(eg: public)。原因:永远不由你代码调用析构器;
Eg:
public ~Tally ( ) {...} //编译时错误
(3)析构器不能获取任何参数。原因:永远不由你代码调用析构器
Eg:
~Tally (int parameter) //编译时错误
3.资源管理:对于宝贵的资源,不能任析构器来释放,需亲自释放资源。为此专门创建一个disposal(处置)方法对资源进行处置,控制合适释放资源。
(注:“disposal方法”强调的是方法的用途而不是名称,换言之一个disposal可以使用任何有效的C#方法名,而不是说专门有一个名为disposal的方法)
4.using语句:提供一个脉络清晰的机制来控制资源的生存期。可以创建一个对象,这个对象会在using语句块结束时销毁。using语句的语法如下:
Using ( tape variable = initialization ) // (类名 对象名称 = 创建对象new)
{
StatementBlock ; //语句块
}
第15章 实现属性以访问字段
1.属性是字段和方法的一个交集–看起来像一个字段,行为上又像一个方法(本质上是方法)。访问一个属性所用的语法和访问一个字段的语法是相同的。然而编译器会将这种字段风格的语法自动转换成对特定的accessor方法的调用(取值和赋值的方法统称为accessor方法)。
属性的旨在向外界隐藏字段的实现。
2.属性(Property)的声明:在结构或类声明一个可读/可写的属性,声明属性的类型、它的名称、一个get accessor和一个set accessor。
AccessModifier Type PropertyName
{
get
{
... //取值代码 (get accessor)
}
set
{
... //赋值代码 (set accessor)
}
}
一个属性包含两个代码块:get 和 set 。其中,get块包含的是在读取属性要执行的语句(取值代码或说get accessor);set块包含的是在向属性写入时要执行的语句(赋值代码或说set accessor)。属性的类型(Type)指定了由get accessor 和 set accessor读取和写入的数据的类型。
3.使用属性:在表达式中使用一个属性,要么对它进行取值(若不需要修改它的值),要么对它进行赋值(若需要修改它的值)。
(1)取值eg:
ScreenPosition origin = new ScreenPosition(0, 0);
int xpos = origin.X; //实际会调用origin.X.get
int ypos = origin.Y; //实际会调用origin.XY.get
对属性取值,编译器会自动将你的字段风格的代码转换成对该属性的get accessor的调用。
(2)赋值eg:
origin.X = 40; //实际会调用origin.X.set,value会设为40
origin.Y = 100; //实际会调用origin.Y.set,value会设为100
要赋的值是通过value变量传给set accessor的。“运行时”会自动完成这个传值过程。
(3)同时对一个属性进行取值/赋值操作。eg:
origin.X += 10;
4.只读/只写属性:
(1)只读属性:声明只包含一个get accessor 的属性,这样就声明了一个只读属性。eg:
struct ScreenPosition
{
...
public int X //大写的X是public属性
{
get { return this.x;}
//未写set {...},也就未有赋值属性
}
}
X属性不含set accessor,所以尝试对X进行写入操作会报告编译错误。
(2)只读属性:声明只包含一个set accessor 的属性,这样就声明了一个只写属性。eg:
struct ScreenPosition
{
...
public int X //大写的X是public属性
{
//未写get {...},也就未有读取属性
set { this.x = rangeChecked(value);}
}
}
X属性不含get accessor,所以尝试对X进行读取操作会报告编译错误。
5.属性的可访问性:属性的可访问性是指它是public、private 还是 protected 的。可访问性是在声明属性的时候指定的。但在属性的声明中可以为get accessor 和set accessor单独指定可访问性。eg:ScreenPosition结构将X和Y属性的set accessor定义成privwte,但get accessor仍然是public的(因为属性就是public的):
struct ScreenPosition
{
...
public int X //属性X指定为public
{
get {return this.x; }
private set { this.X = rangeCheckedX(value); }
}
public int Y //属性Y指定为public
{
get {return this.y; }
private set { this.y = rangeCheckedY(value); }
}
...
private int x, y;
}
为两个accessor定义互不相同的可访问性时,必须遵守:
(1)定义时,只能改变一个accessor的可访问性。改变两个也无意义(eg属性定义为public,但两个accessor定义private)
(2)accessor的访问修饰符(public、private或protected)所指定的可访问性在限制程度上必须大于属性的可访问性。eg:假如将属性定义为private,就不能将get accessor声明为public(相反应该使属性为public,使get accessor 为 private)。
6.属性的局限性:
(1)只有在一个结构或类初始化好了之后,才能通过这个结构和类的属性来进行赋值。eg:
ScreenPosition location;
location.X = 40; //编译时错误,location结构变量尚未使用new初始化
(2)不能将属性作为一个ref或者out参数值传给一个方法;但可以将一个可写的字段作为ref或out参数值传递。这是由于属性并不真正指向一个内存位置;相反,它代表的是一个accessor方法。eg:
MyMethod (ref location.X); //编译时错误
(3)在一个属性中,最多只能包含一个get accessor 和一个set accessor。属性不能包含其他方法、字段或属性。
(4)get accessor 和set accessor不能获取任何参数。要赋的值会通过内建的、隐藏的value变量自动传给set accessor
(5)不能声明const属性。eg:
const int X { get {...} set {...} } //编译时错误
7.在接口中声明属性:除了可以在接口中定义方法之外,还可以定义属性。
为此,需要指定get或set关键字,或者同时指定这两个关键字。但要将get accessor或set accessor的主体替换成一个分号。eg:
interface IScreenPosition
{
int X { get; set; } //无主体
int Y { get; set; } //无主体
}
实现这个接口的任何类和结构都必须实现X和Y属性,并在属性中定义get accessor 和 set accessor。eg:
struct ScreenPosition : IScreenPosition
{
...
public int X
{
get {...}
set {...}
}
public int Y
{
get {...}
set {...}
}
...
}
在类中实现接口属性,可以将属性的实现声明为virtual,这就允许将来派生的类重写(override)这些实现。eg:(这个例子展示的是一个类。virtual关键字的结构(struct)中是无效的,结构是隐式密封(sealed)的。)
struct ScreenPosition : IScreenPosition
{
...
public virtual int X
{
get {...}
set {...}
}
public virtual int Y
{
get {...}
set {...}
}
...
}
还可以选择使用显示接口实现语法来实现一个属性。属性的显示实现是非公共和非虚的(因而不能被重写)。eg:
struct ScreenPosition : IScreenPosition
{
...
int IScreenPosition.X //显示实现接口中的一个属性时,要附加接口名作为前缀
{
get {...}
set {...}
}
int IScreenPosition.Y //显示实现接口中的一个属性时,要附加接口名作为前缀
{
get {...}
set {...}
}
...
private int x, y ;
}
8.出于与应用程序的兼容性和与接口的兼容性两个方面考虑,应该坚持定义属性,而不是将数据作为public字段揭示出来。
但要注意:只要使用了自动生成的属性,就必须同时指定一个get accessor和一个set accessor。自动生成的属性不可以是只读或者只写的。
在类或结构中,定义带有空白的get accessor 和 set accessor的属性。eg:
class Polygon
{
public int NumSides { get; set; }
}
9.使用属性来初始化对象
对象初始化器的语法:eg
Triangle tri1 = new Triangle { Side3Length = 15 };
Triangle tri2 = new Triangle { Side1Length = 15,
SideLength = 20 };
Triangle tri3 = new Triangle { Side2Length = 12,
Side3Length = 17};
Triangle tri3 = new Triangle { Side1Length = 9, Side2Length=12,
Side3Length = 17};
一个重点在于肯定是先运行构造器,再对属性进行设置。(可以理解为先使用(默认)构造器进行初始化,再通过“对象初始化器”更改类中公开的属性的值)