c#入门笔记

来源:互联网 发布:python运维开发面试题 编辑:程序博客网 时间:2024/06/05 07:23


cs中变量名的第一个字符必须是字母、下划线、或者@。 其后的字符可以是字幕,下划线或数字


下面是 camelCase变量名:
age
firstName
timeOfDeath
下面是PascalCase变量名:
Age
FirstName
TimeOfDeath
MS建议:对于简单的变量,使用camelCase规则,而比较高级的命名则使用PascalCase

运算符优先级(由高到低)
++,--(用作前缀);+,-(一元)
* / %
+,-
=,*=,/=,%=,+=,-=
++,--(用作后缀)

namespace 可以对变量进行分类隔离,例如:
namespace level1
{
    int i;
    namespace level3 //这里可以进行嵌套
    {
        int a;
    }
}
如果想在level2中之中level1的i则
namespace level2
{
    //使用level1中的i  这样用 level1.i
    //使用level3中的a 这样用 level1.level3.a
}

创建命名空间后,就可以使用using语句简化对他们包含的名称的访问。实际上,using语句的意思是“我们需要这个命名空间中的名称,所以不要每次总是要求对它们分类”例如,在下面的代码中,levelone命名空间中的代码可以访问levelone.leveltwo命名空间的名称,而无需分类:
namespace LevelOne
{
    using LevelTwo;

    namespace LevelTwo
    {
        // name "NameTwo" defined
    }
}

如果使用using如果有相同的变量出现在一起的时候会发生冲突,此时,可以为命名空间提供一个别名,作为using语句的一部分。
namespace LevelOne
{

    using LT=LevelTwo;
        // name "NameTwo" defined

    namespace LevelTwo
    {
        // name "NameTwo" defined
    }
}
这样可以使用LT.NameTwo来使用leveltwo中的nametwo


在书中的代码中开头都是包含了三个using
using System;
using System.Collections.Generic;
using System.Text;
解释:System命名空间是.NET Framework应用程序的根命名空间,包含控制台应用程序所需要的所有基本功能。其他两个命名空间常常用于控制台程序,所以包含了这三行代码

类、结构或接口成员声明中标记"namespace"无效

&,|与&&,|| 作用一样,但得到的结果方式有一个重要区别:其性能后者比较好。两者都是检查第一个操作数的值,再根据该操作数的值进行操作,可能根本就不需要第二个操作数的值。
如果,&&运算符的第一个操作数是false,就不需要考虑第二个操作数的值了,因为无论第二个操作数的值是真么,其结果都是false。同样,如果第一个操作数是true,||运算符就返回true,无需考虑第二个操作数的值。
但上面的&和|运算符却不是这样。他们操作数总是要计算的。(这个对比上就体现出性能的差别)
作为一个规则,尽可能使用&&和||运算符

布尔赋值运算符
&=   例如 a&=b  结果 a的值等于a&b的结果
|=
^= 类似

注意:尽管上面提到了&,|与&&,||之间的关系 ,但是还要明确,&,|实质上是位运算符,例如 i=2; i=i||0这里i的结果是1 但是如果i=i|0那么i的结果是2!

switch语句中每一个case中都要包含一个break;不允许在执行完一个case语句块之后执行下一个case(这是与c++不同之处 ),但是有一中情况例外,把多个case语句放在一起(堆叠他们),其后加一行代码,实际上是一次检查多个条件。如果满足这些条件的任何一个,就会执行代码,例如:
switch(<testval>)
{
    case <val1>:
    case <val2>:
        <codes..>;
        break;
}

switch 语句中的语句中的case后面要跟着常量,当然也可以是用const修饰的常变量

string name;
name=name.ToLower();//这里就可以将name中的字符全都转换成小写

使用Convert命令进行显示转换

在cs中,枚举的定义
enum typeName
{
    value1,
    value2,
    value3,
    ...
    valueN   //注意这里没有标点 所以可以这样写 enum typeName{val1,val2,val3}
}  //注意 这里没有";" 这是与c的区别,c这里有";"结构体也一样
声明这个新类型的变量
typeName varName;
并赋值
varName=typeName.value; //这里也是与c的区别,c这里不需要加上typeName

枚举使用一个基本类型来存储。枚举类型可以提取的每个值都存储为该机本类型的一个值,在默认情况下,该类型为Int。在枚举中添加类型,就可以指定其他基本类型

enum typeName:underlyingType
{
    value1,
    value2,
    value3,
    ...
    valueN 
}
枚举的基本类型可以是byte,sbyte,short,ushort,int,uint,long,ulong
枚举默认从0开始依次加1,可以赋值,其后未赋值的默认从前加1

应道注意的是,类型定义代码放在命名空间中,不与其他代码放在一起,这是因为在运行期间,定义代码并不是想执行程序中的代码那样一行一行的执行。应用程序是从已经习惯的位置开始执行的。//结构体也是这样

int i=200;
string mystr;
mystr=Convert.ToString(i); //这里可以用变量本身的ToString()命令,例如:
mystr=i.ToString();

结构
定义结构
struct <typeName>
{
    <memberDeclarations>
}
<memberDeclarations>部分包含变量的定义(成为结构的数据成员),其格式与往常一样。每个成员的声明都采用如下形式:
<accessibility><type><name>;//这里的<accessibility>是必须的 不能够省略
要让调用就诶够的代码访问该结构的数据成员,可以对<accessibility>使用关键字public,例如:
struct <typeName>
{
    public double i;
}

数组
声明:
<baseType>[]<name>;
必须初始化
初始化1:使用初始化列表,例如:
int[]array={1,2,4};
初始化2:
int[]array=new int[5];
这里使用关键字new显式的初始化数组,用一个常量定义其大小。这种方式会给所有的数组原副赋予同一个默认值,对于数值类型来说,其默认值是0.
也可以使用非常亮的变量进行初始化,例如:
int[]array=new int[arraySize];
还可以使用这两种初始化方式的组合:
int[]array=new int[5]{1,2,3,4,5}; //使用这种方式初始化数组的大小和元素的个数必须匹配,否则错误

foreach循环
foreach循环可以使用一宗简便的语法定位数组中的每一个元素
foreach(<baseType><name>in<array>)
{
    //can use <name> for each element
}
这个新欢会迭代每个严肃,一次把每个元素放在变量<name>中,且不存在访问非法元素的危险。不需要考虑数组中有多少个元素,并可以确保将在循环中使用每个元素。
使用这种方法与标准的for循环的主要区别是foreach循环对数组内容进行只读访问,所以不能改变任何元素的值。

多维数组
二维数组可以声明如下:
<baseType>[,] <name>;
初始化例如
int[,] array=new int[2][2];
int[,] array={{2,3}{1,4}};// 特别注意,CS多维数组的引用与c完全不同,例如cs的二维数组的引用array[2,3] 而不是array[2][3]
多维数组只需要更多的逗号,例如:
<baseType>[,,,] <name>;

数组的数组
以上的数组都是定长的数组,也就是每一行元素的个数都是确定的。也可以使用变长数组,其中每行都有不同的元素个数。为此,需要有这样一个数组,其中的每个元素都是另一个数组。也可以有数组的数组的数组,或更加复杂的数组。但是,注意这些数组都必须有相同的基本类型。
声明数组的数组,其语法要在数组的声明中指定多个方括号对,例如:
int[][]=jaggedIntArray;
初始化的两种形式:
jaggedIntArray=new int[2][];
jaggedIntArray[0]=new int[3];
jaggedIntArray[1]=new int[4];
也可以使用上述字面赋值的一种改进形式
jaggedIntArray=new int[3][]{new int[]{1,2,3},new int[]{1},new int[]{1,2}};
也可以简化,把数组的初始化和声明放在一行上,如下所示:
int[][]jaggedIntArray={new int[]{1,2},new int[]{3}};
数组的数组的引用例如 strarray[0][0]; //这和普通的数组不一样,普通的数组只有一个[]对

对变长数组可以使用foreach循环,但通常需要嵌套该循环,才能得到实际的数据,例如:
int[][] jaggedIntArray={new int[]{1},new int[]{1,2,4},new int[]{2,3}};
foreach(int[] i in jaggedIntArray)//foreach是遍历数组中的每一个元素,这里jaggedIntArray数组的元素的类型实际上是int[].注意!
{
    for(int j in i)
    {
        Console.WriteLine(j);
    }
}

字符串的处理

string可以看成是char变量的只读数组,只能读访问,不能改变。

为了获得一个可写的char数组,可以使用下面的代码,其中使用了数组变量的ToCharArray()命令:
string myString="hello";
char[] array=myString.ToCharArray();//注意! 这里ToCharArray命令将5个字符赋值给了array数组,即array数组的个数是5

string i="hello"+" "+"world";//则i就是"hello world”,这也是字符串相加的法则

与数组一样,还可以使用myString.Length获取元素的个数,这将给出字符串中字符的个数,例如:
string myString=Console.ReadLne();
Console.WriteLine("You typed {0} characters.",myString.Length);

还有比较有用的命令:<string>.ToLower()和<string>.ToUpper() 这两个命令可以统一格式
//注意:这两个命令在赋值有用。例如 str2=str1.ToUpper();有用 而单独的str1.ToUpper()是没有意义的
//这些命令只是起到一个临时转化的作用,对于调用他们的<string>没有任何的改变

<string>.Trim() 命令用来去掉用户无意间在输入内容的前面或者后面添加了额外的空格。//这就是起到了去头掐尾的作用,对于字符串中间的空格不起作用
Trim命令默认情况下是删除空格,但是本身他的参数是char[]类型,所以可以加入字符数组用于删除头尾中与数组匹配的所有字符(hell 如果字符数组中包含l 那么则删除2个l)例如:
str="hello";
char[] ch={'h','o'};
str=str.Trim(ch);//结果是str变为"ell"

<string>.TrimStart()与<string>.TrimEnd()作用与Trim类似,只是作用范围不同,其参数也是char[]

<string>.PadLeft() 与<string>.PadRight()作用是在字符串的左边或右边添加空格,语法:
<string>.PadLeft(<desiredLength>);
默认是加空格,也可以是别的字符,这里他们的参数是char类型(而不是char[])类型,例如
<string>.PadLeft(<desiredLength>,'c');

<string>.Split(char[]) 命令的作用是将string转换成string数组,把它在指定的位置分开。这些位置采用char数组的形式。例如:
string[] str1;
char[] ch={' '};
string str2="hello world";
str1=str2.Split(ch); //这里就用' '把str2分成了两部分,赋值给数组str1,即现在str1中有两个元素“hello”"world"

<string>.IndexOf(char ch) 这个命令的功能是在字符串中寻找跟ch匹配的值,并返回其位置(int类型),默认第一个字符的位置为0,如果没有匹配的则返回-1....这是指IndexOf的一种情形,其还有中重载形式,例如寻找指定的字符串

函数
(自己的理解)貌似在cs中函数定义的位置可以任意,不必可以要求定义在调用之前,或者在调用之前声明,在c中如果fun函数在main后定义的 如果在main中调用需要在main之前做一下void fun();的声明,经过试验在cs中没有这样的要求,反而声明会出现错误

函数名一般采用PascalCase形式来编写

函数都要有个return作为出口,void类型除外

函数参数的个数,类型,次序必须严格匹配

参数数组
cs允许为函数指定一个(只能指定一个)特定的参数,这个参数必须是函数定义中的最有一个参数,成为参数数组。参数数组可以使用个数补丁的参数调用函数,它可以使用params关键字来定义。
参数数组可以简化代码,因为不必从调用代码中传递数组,而是传递可在函数中使用的一个数组中相同类型的几个参数。
定义使用参数数组时,需要使用下述代码:
static <returnType><functionName>(<p1Type><p1Name>,...,params<type>[]<name>)
{
    ...
    return <returnValue>;
}
使用下面的代码可以调用该函数。
<functionName>(<p1>,..,<val1>,<val2>,...);
其中<val1>,<val2>等都是类型为<type>的值,用于初始化<name>数组。在可以指定的参数个数方面没有限制。甚至可以根本不指定参数。唯一的限制是他们都必须是<type>类型。例如:
static void fun(string str, params int[] n)
{
    string[] hello;
    char[] ch={' '};
    hello = str.Split(ch);
    foreach (int i in n)
    {
        Console.WriteLine(hello[i-1]);
    }
                                                                            }
如果调用函数,fun("hello i'm terry",1,3); 则会输出hello terry

引用(reference)调用参数:定义、调用函数的时候需要加上关键字ref,例如:
static void fun(ref int i){}//定义
fun(ref n);//调用
引用的两个限制:
引用变量是在该函数中需要改变值的,如果调用的时候是常变量是非法的,例如:
const int i;
fun(ref i); //非法!因为i用const修饰的常变量
其次,必须使用初始化过的变量,cs不允许假定ref参数在使用它的函数中初始化,下面的代码也是非法的;
int i;
fun(ref i);//非法!因为i在作为引用参数之前没有被初始化

输出参数:使用关键字out,使用方法与ref相同
与ref的重要区别:
1.把未赋值的变量用作ref参数是非法的,但可以把未赋值的变量用作out参数。
2.在使用out参数时,该参数必须看作是还未赋值。即调用代码可以把已赋值的变量用作out参数,存储在该变量中的值会在函数执行时丢失;例如:
int i=2;
fun(out i); //在fun中必须给i赋值,这个赋的值就会编程这个实参的值

全局变量:在控制台应用程序中,必须使用static或const关键字,来定义这种形式的全局变量。如果要修改全局变量的值,就需要使用static,因为const禁止修改变量的值//自己测试了一下 这里的static不能像c那样用在函数中作为一种静态一次初始化的那种变量

在局部变量与全局变量共存的时候需要使用完整限定的名称,例如
class Pragram
{
    static i=1;
    static void Main()
    {
        int i=2;
        i //这里就是2
        Pragram.i //这里就是1
    }
}

要重视一个问题,如果只是int i;这是声明,并没有给i分配内存空间,所以一直要求给i进行初始化。例如下面就是非法的
int i;
for(int j=9;j++)
{
    i=2;
}
Console.WriteLine("{0}",i); //这里非法!因为i没有定义,在for中进行的初始化是局部类型的,出了for就消失了,类似的还有if,while语句块等,当然i可以是任何类型的 //如果在声明的时候一起初始化那么就合法了int i=0或者在for外单独初始化

结构函数
cs中的结构不仅可以存放数据,而且可以存放函数,例如
struct hello
{
    public void fun()
    {...}
}
hello i;
i.fun(); //这里就可以调用函数。。。注意!结构体hello中所有的数据元素对于函数来说都是全局变量,可以直接调用

函数的重载
函数名甚至函数类型也相同,可以通过参数的个数,类型,次序的不同进行区别,在调用的时候根据实参的实际情况自动区别。

委托
定义(在函数的外面,例如在Main函数中定义会提示错误),关键字delegate:
delegate <type><delegateName>(参数);
定义变量
<delegateName> <name>;
赋值
<name>=new <delegateName>(函数名)//注意!这里只需要函数名,且委托的函数参数,类型必须与委托定义的时候形式一致.
使用
<name>(参数); //这里就可以用<name>取代所委托的函数名来调用函数
委托的变量还可以用作函数的参数,例如:
fun(<delegateName><name'>)
{
    <name'>();//这样就可以在函数fun中调用<name'>委托的函数,当然这里<name'>只是形参
}

面向对象简介

在UML中每个参数都带有下述标识符之一:in、out或inout。它们用于表示数据流的方向,其中out和inout大致对应cs中的关键字ref和out,in大致对应cs中不使用这两个关键字的情况

在cs中构造函数用new关键字来调用

静态成员可以在雷的实例之间共享,所以可以将他们看作是类的全局对象。静态属性和静态字段可以访问独立于任何对象实例的数据,静态方法可以执行与对象类型相关、但与对象实例无关的命令。在使用静态成员时,甚至不需要实例化对象。

静态类
我们常常希望类只包含静态成员,且不能用于实例化对象(例如Console)。为此一种简单的方法是使用静态类,而不是把类的构造函数设置为私有。静态类只能包含静态成员,不需要构造函数的定义,因为按照定义,它根本不能实例化。

接口
接口是把隐式公共方法和属性组合起来,以封装特定功能的集合。一旦定义了接口,就可以在类中执行它。这样,类就可以支持接口所指定的所有属性和成员。
注意,接口不能单独存在。不能像实例化一个类那样实例化接口。另外,接口不能高喊执行其成员的任何代码,而只能定义成员本身。执行过程必须在执行接口的类中实现。

接口的名称一般用大写字母I开头

接口的成员是公共的(因为它们更倾向于在外部使用)

接口页可以派生于其他接口。与类不同的是,接口可以派生于多个基接口(此类可以支持多个接口的方式派生)
继承
注意:cs中的对象只能直接派生于一个基类,当然基类也可以有自己的基类
派生类不能访问基类的私有成员,但可以访问其公共成员。不过,派生里和外部的代码都可以访问公共成员。这就是说,自能使用这两个可访问性,不能让一个成员可由基类和派生类访问,而不能由外部代码访问。
为了解决这个问题,cs提供了第三种可访问性:protected,只有派生类才能访问protected成员,对于外部代码,这个可访问性与私有成员一样:外部代码不能访问private成员和protect成员

多态性
可以用派生类对象初始化基类的对象,但是基类的对象只能操作基类的数据和方法例如:
class fulei
{public void fun(){}}
class zilei:fulei
{public void fun(){}
 public void fun2(){}}
如果这样操作
zilei zi=new zilei();
fulei fu=zi;
fu.fun();//这里其实是调用了基类就是这里的fulei中的fun函数,不是调用基类的函数
----fu.fun2();这是非法的,因为fulei中没有fun2的实现
注意!:这里只能用派生类的对象初始化基类的对象,不能用基类的对象初始化派生类的对象(可能是因为派生类中某些内容是基类没有的)


可访问性:
综上可得:
基类:public,private,protect
派生类:public,private
外部的代码:public

 

定义类

在默认情况下,类声明为内部的,即只有当前项目中的代码才能访问它。可以用internal访问修饰符关键字显示指定,如下所示(但这是不必要的):
internal class hello
{}
另外,还可以指定类是公共的,应可以有其他项目中的代码访问,关键字public
public class hello
{}//注意,以这种方式声明的类不能是私有保护的。可以把这些声明类的修饰符用于声明类成员

可以指定类是抽象的(不能实例化,只能继承,可以有抽象成员)或密封的(sealed,不能继承)。为此,可以使用两个互斥的关键字abstract或sealed,例如:
abstract class haha
{}
sealed class hahb
{}

可以在类的定义中指定继承。为此,要在雷鸣的后面加上冒号,其后是基类名,例如
public class MyClass:MYbase
{}
注意!在cs的类定义中,只能有一个基类,如果继承了一个抽象类,就必须实现所继承的所有抽象成员(除非派生类也是抽象的)

编译器不允许派生类的访问型比其基类更高。也就是说,内部类可以继承于一个公共类,但公共类不能继承于一个内部类。

如果没有使用基类,则被定义的类就只继承于基类System.Object(它在CS中的别名是object)。毕竟在继承层次结构中,所有类的跟都是System.Object

除了以这种方式指定基类外,还可以在冒号后面指定支持的接口。如果制定了基类,他必须紧跟在冒号的后面,之后才是指定的接口。如果没有指定基类,则接口就跟在冒号的后面,必须使用逗号分隔基类名(如果有基类)和接口名

【所有的接口成员都必须在支持该接口的类中实现,实现方法的时候需要用关键字override】,但如果不想使用给定的接口成员,就可以提供一个"空"的执行方式(没有函数代码)。还可以把接口成员实现为抽象类中的抽象成员.---------------!!!!!这个规范很重要

接口的定义
声明接口的方式与声明类的方式相似,但使用的关键字是interface,而不是class,例如:
interface IMyInterface
{
    //Interface members.
}
定义接口的时候,方法不需要用访问权限,只要类型和名字即可,例如 void fun(); 属性用类型名和名字即可,但需要包含set;get; 例如 string Name{get;set;}
关键字abstract和sealed不能在接口中使用,因为这两个修饰符在接口定义中没有意义(它们不包含执行代码,所以不能直接实例化,且必须是可以继承的)
接口的继承也可以用与类继承的类似方式来指定。主要的区别是可以使用多个基接口,例如:
public interface IMyInterface:IMyBaseInterface,IMyBaseInterface2
{
    //Interface members.
}

(自己的理解)对于接口
接口给程序提供了一个统一的模板,是一个抽象化的东西,就像一个没有实际内容的模型,可以看做是一个占位符,其本身没有意义 例如:
class Program
{
    static void Main(string[] args)
    {
        I inter;
        A a=new A();
        B b=new B();
        inter = b;//这里直接将对象赋值给接口(实质上这是利用了接口的多态性)
        inter.fun();//这里就是一个占位符,因为上面赋值了b所以有了实际的意义
        fun(b);
        Console.ReadKey();
    }
    static fun1(I inter)//形参的方式赋值
    {
        inter.fun(); //这里也是占位符
    }
}
interface I
{ void fun();}
class A : I
{
    public void fun()
    { Console.WriteLine("A"); }
}
class B : I
{
    public void fun()
    { Console.WriteLine("B"); }
}
这样看,接口的作用就是给定了一个同意的形式,使得程序的扩展型提高,例如上面的程序再扩展一个C类就是很简单的事情了

构造函数的执行序列

如果对一个类使用非默认的构造函数,默认的情况是在其基类上使用匹配这个构造函数签名的构造函数。如果没有找到这样的构造函数,就使用基类默认构造函数(根类System.Object总要使用默认的构造函数,因为这个类没有非默认的构造函数)。*******************(这里提到的从基类上调用与派生类匹配的构造函数在VS2010中不能够实现,但是可以通过在需要的构造函数后面加:base(),以base的参数个数作为区分调用基类相应的构造函数)

如果在派生类的构造函数后面加入关键字:base(),就可以以base的参数作为区分调用相应的基类的构造函数,例如:
class MyBaseClass
{
    public MyBaseClass()-----构造函数1
    {Console.WriteLine("base kong"); }
    public MyBaseClass(int i)------构造函数2
    { Console.WriteLine("int i{0}",i);}
}
class MyDerivedClass:MyBaseClass
{
    public MyDerivedClass()
    {Console.WriteLine("der kong");}
    public MyDerivedClass(int i):base(i)//如果执行这个构造函数,则调用基类的构造函数2,如果没有":base(int i)"那么则会调用构造函数1
    {Console.WriteLine("der int i");}
    public MyDerivedClass(int i,int j):base (10)//如果执行这个构造函数,同样会调用构造函数2
    {Console.WriteLine("der int i intj");}
}

除了base关键字外,这里还可以使用另一个关键字this。这个关键字指定在调用指定的(就是:this关键字跟着的那个构造函数)前,.NET实例化过程对当前类使用非默认的构造函数。例如:
class MyBaseClass
{
    public MyBaseClass()
    {Console.WriteLine("base kong"); }
    public MyBaseClass(int i)
    { Console.WriteLine("int i{0}",i);}
}
class MyDerivedClass:MyBaseClass
{
    public MyDerivedClass():this(2,3)
    {Console.WriteLine("der kong");}
    public MyDerivedClass(int i):base('a')
    {Console.WriteLine("der int i");}
    public MyDerivedClass(int i,int j):base (i)
    {Console.WriteLine("der int i intj");}
}//如果实例化的时候 MyDerivedClass hello=new MyDerivedClass();构造函数的执行顺序是
执行 System.Object.Object()构造函数
执行MyBaseClass(int i)构造函数
执行MyDerivedClass(int i,int j)构造函数
执行MyBaseClass()构造函数

 

单一的顺序是先执行基类的构造函数然后在执行派生类的构造函数。即从System.Object的构造函数开始依次往下执行。

实现构造函数不需要加类型名

结构和类非常相似,但结构是值类型,而类是引用类型。
对象是引用类型。在把对象赋值给变量时,实际上把带有一个指针的变量赋给了该指针所指向的对象。在实际代码中,指针是内存中的一个地址。在这种情况下,地址是内存中该对象所在位置。在使用下面的代码把第一个对象引用付给类型为MyClass的第二个变量时,实际上是复制了这个地址
MyClass objectB=objectA;
这样两个变量就包含同一个对象的指针。
结构是值类型。其变量并不是包含结构的指针,而是包含结构本身。在用下面的代码把第一个结构付给类型为myStruct的第二个变量时,实际上把第一个结构的所有信息复制到另一个结构中
myStruct structB=structA;
这个过程与简单变量类型如int是一样的。最终的结果是两个结构类型量包含不同的结构

接口和抽象类比较

相同点:抽象类和接口都包含可以由派僧类继承的成员。接口和抽象类都不能直接实例化,但可以声明它们的变量。如果这样做,就可以使用多态性把继承这两种类的对象指定给它们的变量。接着通过这些变量来使用这些类型的成员,但不能访问派生对象的其他成员。

不同点:派生类只能击沉给一个基类,即只能继承一个抽象类(但可以用一个继承链包含多个抽象类)。相反,类可以使用任意多个接口。但不会产生太大的区别----这两种情况得到的效果是类似的。只是采用接口方式略有不同。
抽象类可以拥有抽象成员(没有代码体,且必须在派生类中实现,否则派生类本身必须也是抽象的)和非抽象成员(他们拥有代码体,也可以是虚拟的,这样就可以在派生类中重写)。另一方面,接口成员必须都在使用接口的类上实现--他们没有代码体。另外,按照定义,接口成员是公共的(因为他们倾向于在外部使用),但抽象类的成员可以使私有的(只要他们不是抽象的)、受保护的、内部的或受保护的内部成员(其中,受保护的内部成员只能在应用程序的代码或派生类中访问)。此外,接口不能包含字段,构造函数,析构函数或常量。

注意:这说明两种类型用于完全不同的目的。抽象类主要用作对戏那个系列的基类,共享某些主要特性,例如共同的目的和结构。接口则主要用于类,这些类在基础水平上有所不同,但可以完成某些相同的任务。

 

定义类成员

成员定义
public 成员可以由任何代码访问。
private成员只能由类中的代码访问(如果没有使用任何关键字,就默认使用这个关键字)
internal成员只能由定义他的项目(程序集)内部的代码访问。
protect成员只能由类或派生类中的代码访问。。在vs2010中试验派生类只能直接引用静态成员

字段,方法和属性都可以使用关键字static来声明,这表示他们是类的静态成员,而不是对象实例的成员。

定义字段
例如:
class MyClass
{
    public int MyInt;
}
.NET Framework 中公共字段定义以PascalCase形式命名,私有字段通常用camelCase形式命名

字段也可以使用关键字readonly,表示这个字段只能在执行构造函数的过程中赋值,或初始化赋值语句赋值

定义方法
也可以使用下列关键字
virtual ---方法可以重写。
abstract---方法必须在非抽象的派生类中重写(只用于抽象类中)
override---方法重写了一个基类方法(如果方法被重写,就必须使用该关键字)---试验:派生类中不能直接实现包含在抽象基类中的方法,只能重写,所以这时候必须用这个关键字
extern---方法定义放在其他地方

如果使用了override,也可以使用sealed指定在派生类中不能对这个方法进一步修改,即这个方法不能由派生类重写例如:
public class zilei:fulei
{
    public override sealed void fun()
    {}
}

在设计基类的时候,如果预想到某些方法会被重写可以定义为virtual,这样可以写实现,如果定义为abstract则不能写实现

如果基类用virtual实现了一个fun()函数,在派生类中override重写了这个函数的实现,那么在用多态性质初始化基类的对象调用fun()将调用这个重写的fun函数,例如
class fulei
{ public virtual void fun() { Console.WriteLine("fulei"); } }
class zilei:fulei
{ public override void fun() { Console.WriteLine("zilei"); } }
在main函数中
zilei zi=new zilei();
fulei fu=zi;
fu.fun();//这里输出zilei,如果zilei中没有override则输出fulei

如果派生类没有继承接口且基类继承了接口,那么派生类隐式继承了基类继承的接口,如果用派生类的对象“初始化”接口那么通过这个接口调用的方法是基类的,如果派生类直接派生了这个接口,那么可以通过这个接口调用派生类实现的接口

调用重写或隐藏的基类方法
无论是重写成员还是隐藏成员,都可以在类的内部访问基类成员。在很多情况下都很有用的,例如;
1.要对派生类的用回隐藏继承的公共成员,但能访问类中访问其功能。
2.要给击沉过的虚拟成员添加代码,而不是简单的用新的重写的爱吗替换它
为此可以使用base关键字,它表示包含在派生类中的基类的执行代码(在控制构造函数中,其用法是类似的),例如:
class fulei
{ public virtual void fun() { Console.WriteLine("fulei"); } }
class zilei:fulei
{ public override void fun() { Console.WriteLine("zilei"); base.fun(); } }
调用zi.fun();则输出
fulei
zilei
Base使用对象实例,所以在静态成员中使用它会产生错误

this关键字
与base一样,this也可以用在类成员的内部,且该关键字也引用对象实例。由this引用的对象实例是当前的对象实例(即不能在静态成员中使用this关键字,因为静态成员不是对象实例的一部分)
this关键字最常用的功能是把一个当前对象实例的引用传递给一个方法,例如:
public void doSomethin()
{
    MyTargerClass myObj=new MyTargerClass();
    myObj.DoSomethingWith(this);
}
其中实例化的MyTargerClass有一个方法DoSomethingWith(),该方法带一个参数,其类型包含上述方法的类兼容。这个参数类型可以是类的类型,而类的类型有这个类继承,是由这个类或System.Object执行的一个接口

显示执行接口成员
接口成员可以由类显示的执行。如果这么做,该成员就只能通过接口来访问,不能通过类来访问。例如
class myclass:Iinter
{
    void Iinter.dosomething()
    {}
}

属性是控制对数据字段的访问、封装字段的安全性和正确性

利用多态性用派生类对象初始化基类对象,这时候用对基类对象的操作默认是进行基类的方法。但是可以用强制转换的形式调用派生类的方法,例如
fulei fu=new zilei();//或者 fulei fu=zi;
((fulei)fu).fun();//注意这里用了两层括号,如果只用一层就表示为将fu.fun()整个的强制转换 ,显然是非法的。


集合


ArrayList类包含在命名空间 System.Collection.ArrayList中
使用发放
ArrayList myarraylist=new ArrayList();
myarraylist.add(zi);
((zilei)myarraylist[0]).fun();
可以用count方法求出myarraylist中元素的个数
使用Remove()和RemoveAt()方法删除项,例如:
myarraylist.Remove(mycow);
myarraylist.RemoveAt(0);
在执行删除方法字后,ArrayList的对象中元素的数目会自动改变,且排列顺序的绝对位置发生了改变,但相对位置不变
AddRange()方法一次添加好几个项,例如
myarraylist.AddRange(myarray);
IndexOf();方法可以返回对象引用在ArrayLis中的索引位置

定义集合
一种方式是手动执行需要的方法,但比较费时间,复杂,所以可以从一个类中派生自己的集合,例如Symtem.Collections.CollectionBase类,这个类中只提供了一些要求的执行代码,如果要使用提供的功能,就需要自己执行其代码。
为了便于完成任务,CollectionBase提供了两个受保护的属性,它们可以访问储存的对象本身。我们可以使用List和InnerList,List可以通过IList接口访问项,InnerList则是用于储存项的ArrayList对象。例如:
 public class Animals:CollectionBase
{
    public void Add(Animal newAnimal)
    {
        List.Add(newAnimal);
    }
    public void Remove(Animal oldAnimal)
    {
        List.Remove(oldAnimal);
    }
}                                                 
这样实现的一个集合是不提供索引符访问功能的,例如下面的代码就还不能实现
animalCollection[0].Feed();

索引符
索引符是一种特殊类型的属性,可以把它添加到一个类中,一提供类似于数组的访问。实际上,可以通过索引符提供更复杂的访问,因为我们可以定义和使用复杂的参数类型和方括号语法。它最常见的一个用法是对想执行简单的数字索引。例如
public class Animals:CollectionBase
{
    ...
    public Animal this[int animalIndex]
    {
        get
        {
            return (Animal)List[animalIndex];
        }
        set
        {
            List[animalIndex]=Value;
        }
    }
}
如果这个例子要遍历Animals myAnimals的所有成员使用foreach就可以了:
foreach(Animal n in myAnimals)
{}
this关键字与方括号中的参数一起使用,但这看起来类似于其他属性。这个语法是富有逻辑的,因为在访问索引符时,将使用对象名,后跟放在方括号中的索引参数(例如MyAnimals[0])
这段代码对List属性使用一个索引符(即在IList接口上,可以访问CollectionBase中的ArrayList,ArrayList储存了项):
return (Animal)List[animalIndex];
这里需要进行显式数据类型转换,因为IList.List属性返回一个Symtem.Object对象。
注意,我们为这个索引符定义了一个类型。使用该索引符访问一个项时,就可以得到这个类型,即可以编写下述代码:
animalCollection[0].Feed();
而不是
((Animal)animalCollection[0]).Feed();
这是强类型化定制集合的另一个方便特性。

关键字索引(继承类DictionaryBase)
public class Animals:DictionaryBase
{
  public void Add(string newID, Animal newAnimal)
  {
    Dictionary.Add(newID,newAnimal);
  }
  public void Remove(string animalID)
  {
    Dictionary.Remove(animalID);
  }
  public Animal this[string animalID]
  {
    get
    {
      return (Animal)DictionaryBase[animalID];

    }
    set
    {
      Dictionary[animalID]=value;
    }
  }
}
基于DictionaryBase的集合和基于CollectionBase的集合之间的一个区别是foreach的方式略有区别。CollectionBase集合可以直接提取对象,使用foreach和DictionaryBase
派生类可以提供DictionaryEntry结构,这是在System.Collections命名空间中定义的另一个类型。要得到Animal对象本身,就必须使用这个结构的Value成员,也可以使用结构的Key成员得到相关的关键字。 例如 :
foreach(DictionaryEntry n in myAnimals)
{
  ((Animal)DictionaryEntry.value).fun();
}

派生类对象可以转换为基类对象,但是基类的对象不能转化为派生类的对象。

在属性set中的value关键字表示用户提供的值

迭代器

定义:它是一个代码块,按顺序提供了要在foreach循环中使用的所有值。一般情况下,这个代码块是一个方法,但也可以使用属性访问其和其他代码块作为迭代器。只是方法比较简单。
无论代码块是什么,其返回值类型都是有限制的。与期望正好相反,这个返回类型与所枚举的对象类型不同。例如,在表示Animal对象集合的类中,迭代器块的返回类型不可能是Animal。两种可能的返回类型是前面提到的接口类型IEnumerable和IEnumerator.使用这两个类型的场合是:
1.如果要迭代一个类,可以使用方法GetEnumerator();返回类型是IEnumerator。
例如在Animals集合类中加入下面的语句:
public new IEnumerator GetEnumerator() //这里是覆盖掉原来的方法,名字一定要对
{
  foreach(object animal in Dictionary.Values); //这里object是所有类型的基类(无敌的东西),注意这里的values
   yield return(Animal)animal;//这里要进行类型转换

}
2.如果要迭代一个类成员,例如一个方法,则使用IEnumerable。
例如:
public static IEnumerable SimpleList()
{
  yield return "string 1";
  yield return "string 2";
}
public static void Main(string[] args)
{
  foreach(sting n in SimpleList()); //这里有个()不要忘记
  {...}
}

在迭代块中,使用yield关键字选择要在foreach循环中使用的值。其语法如下:
yield return value; //这里的关键字是组合的 yield return

object是所有类型的基类

浅度复制
例如在一个类中加入方法
public object GetCopy() //这里的GetCopy可以是任意命名的方法名,还有object类型
{
  return MemberwiseClone(); //这里是使用了System.Object.MemberwiseClone()进行浅度复制。所以不能改变格式
}
使用:
MyClass object1=new MyClass();
MyClass object2=(MyClass)object1.GetCopy();//注意,因为返回了一个object类型所以要进行类型的转换
对于对象中的值类型,用这个浅度复制会生成一个新的副本,即object1和object2中值类型是互相独立的。但是对于对象中的引用类型(例如:类)那么经过浅度复制出的新对象中这个引用类型还是指向了原来的引用对象。
下面是把利用浅度复制复制了一个类的例子,注意复制的类型和MemberwiseClone之前的类型转换
class lei1
{ public int i;}
class lei2
{
  public int i;
  public lei1 haha = new lei1();
  public lei2 copy()
  { return (lei2)MemberwiseClone(); }
}

深度复制
使用接口ICloneable 该接口有一个方法Clone(),这个方法不带参数,返回一个对象类型。示例源码:
public class Content
{ public int Val; }
public class Cloner : ICloneable
{
  public Content MyContent = new Content();
  public Cloner(int newVal)
  { MyContent.Val = newVal; }
  public object Clone()
  {
    Cloner clonedCloner = new Cloner(MyContent.Val);
    return clonedCloner;
  }
}
示例源码二,集合类的深度复制
public class Card:ICloneable
{
  public object Clone()
  { return MemberwiseClone(); }
}
public class Cards : CollectionBase, ICloneable
{
  public object Clone()
  {
    Cards newCards = new Cards();
    foreach (Card sourceCard in List)
    { newCards.Add(sourceCard.Clone() as Card); }
    return newCards;
  }

}

比较
类型比较
if(myObj.GetType()==typeof(MyComplexClass))
{
  //myObj is an instance of the class MyComplexClass.
}

封箱和拆箱
封箱(boxing)是把值类型转换为System.Object类型,或者转换为由值类型实现的接口类型。拆箱(unboxing)是相反的转换过程。
拆箱一个值需要进行显式转换,既需要惊醒数据类型转换,封箱是隐式的,所以不需要进行数据类型转换.
封箱的作用:首先,它允许使用集合中的值类型(例如ArrayList),集合中项的类型是object。其次,有一个内部机制允许在值类型上调用object,例如int 和结构

is运算符
is运算符并不是说明对象是某种类型的一种方式,二是可以检查对象是否是给定的类型,或者是否可以转换为给定的类型,如果是,这个运算符就返回true。
语法:
<operand>is<type>
表达式的结果如下:
1.如果<type>是一个类类型,而<operand>也是该类型,或者它继承了该类型,或者它封箱到该类型中,则结果为true.
2.如果<type>是一个接口类型,而<operand>也是该类型,或者它是实现该接口的类型,则结果为true.
3.如果<type>是一个值类型,而<operand>也是该类型,或者它被拆箱到该类型中,则结果为true.


值比较
运算符重载
要重载运算符,可给类添加运算符类型成员(他们必须是static).一些运算符有多种用途,例如-运算符就有一元和二元两种功能,因此我们还指定了要处理多少个操作数,以及这些操作数的类型.一般情况下,操作数的类型与定义运算符的类相同,但也可以定义处理混合类型的运算符.
示例源码:
public class AddClass1
{
  public int val;

  public static AddClass1 operator +(AddClass1 op1, AddClass1 op2)//operator是运算符重载的key
  {
    AddClass1 returnVal = new AddClass1();
    returnVal.val = op1.val + op2.val;
    return returnVal();
  }
}
上面的这个例子是二元重载,当然也可以进行一元重载,只需将参数设为一个即可。
这是个操作数的类型与类相同,返回值也是该类型。但是我们也可以使用混合类型的重载运算符,例如
public class AddClass1
{
  public int val;

  public static AddClass3 operator +(AddClass1 op1, AddClass2 op2)//operator是运算符重载的key
  {
    AddClass1 returnVal = new AddClass1();
    returnVal.val = op1.val + op2.val;
    return returnVal();
  }
}
public class AddClass2
{
  public int Val;
}
public class AddClass3
{
  public int Val;
}
这里需要注意的是:
1.例如上面的例子中,如果将+的重载也在AddClass2中再写一遍,就会出现错误,也就是说,应注意不要把签名相同的运算符添加到多个类中。
2.运算符重载代码的位置应是操作数类中的任意一个,上面的代码如果在AddClass3就会出错。
3.如果混合了类型,操作数的顺序必须与运算符重载的参数顺序相同。

可以重载的运算符:
一元运算符:+,-,!,~,++,--,true ,false
二元运算符:+,-,*,/,%,&,|,<<,>>
比较运算符:==,!=,>,<,<=,>=
注意,一些运算符如<和>需要成对重载

IComparable和IComparer接口
IComparable AND IComparer接口是.NET Framework中比较对象的标准方式。这两个接口之间的区别如下:
1.IComparable在要比较的对象的类中实现,可以比较该对象和两翼个对象。
2.IComparer在一个单独的类中实现,可以比较任意对象.
ICloneable提供了一个方法CompareTo().这个方法接受一个对象,所以可以实现它。
IComparer提供了一个方法Compare()。这个方法接受两个对象,返回一个整形结果,这与CompareTo相同
在这两种情况下,提供给方法的参数是System.Object类型。也就是说,可以比较任意类型的两个对象。所以,在返回结果之前,通常需要进行某种类型比较,如果使用了错误的类型,还会抛出异常。
在.NET Framework在类Compare上提供了IComparer接口的默认实现方式,类Compare位于System.Collections命名空间中,可以对简单类型以及支持ICloneable接口的任意类型进行特定文化的比较。例如 Compare.Default.Compare(obj1,obj2)
使用了ICloneable接口的类,要在内部实现:
public int CompareTo(object obj)
{
  if(obj is Person)//这里进行类型的判断
    {}
  else{}//这里可以用来当类型不匹配的时候抛出异常
}

使用ICloneable AND IComparer接口对集合排序
许多集合可以用对象的默认比较方式进行排序,或者用定制的方法来排序。ArrayList就是一个示例,它包含方法Sort(),这个方法使用时可以不带参数,此时使用默认的比较方式,也可以给它传送IComparer接口以比较对象。
注意,System.Collection命名空间中的一些类.包括CollectionBase,都没有提供排序方法。如果要对派生于这个类的集合排序,就必须多做一些工作,自己给内部的List集合排序。
示例源码:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections;

namespace ConsoleApplication3
{
  class Program
  {
    static void Main(string[] args)
    {
      ArrayList list = new ArrayList();
      list.Add(new Person("jim", 30));
      list.Add(new Person("bob", 25));
      list.Add(new Person("bert",27));
      list.Add(new Person("ernie",23));
      Console.WriteLine("UNSORTED PEOPLE:");
      for (int i = 0; i < list.Count; i++)
      {
        Console.WriteLine("{0}{1}", (list[i] as Person).Name, (list[i] as Person).Age);
      }
      Console.WriteLine();

      Console.WriteLine("people sorted with default comparer (by age)");
      list.Sort(); //这里实行的是进行默认的排序方式,这里所谓的默认就是指继承IComparable接口的Person类中实现的CompareTo方法
      for (int i = 0; i < list.Count; i++)
      {
        Console.WriteLine("{0}{1}", (list[i] as Person).Name, (list[i] as Person).Age);
      }
      Console.WriteLine();

      Console.WriteLine("people sorted with nondefaule comparer(by name)");
      list.Sort(PersonCompareName.Default); //这里给Sort方法传递了一个IComparer接口
      for (int i = 0; i < list.Count; i++)
      {
        Console.WriteLine("{0}{1}", (list[i] as Person).Name, (list[i] as Person).Age);
      }
      Console.WriteLine();
      Console.Read();
    }
  }
  class Person : IComparable
  {
    public string Name;
    public int Age;
    public Person(string name, int age)
    {
      Name = name;
      Age = age;

    }
    public int CompareTo(object obj)//
    {
      if (obj is Person)
      {
        Person otherPerson = obj as Person;
        return  this.Age - otherPerson.Age;
      }
      else
      {
        throw new ArgumentException("object to compare to is not a persong object");
      }
    }
  }
  public class PersonCompareName : IComparer
  {
    public static IComparer Default=new PersonCompareName();
    public int Compare(object x,object y)
    {
      if(x is Person&&y is Person)
      {
        return Comparer.Default.Compare(((Person)x).Name,((Person)y).Name);
      }
      else{throw new ArgumentException("ome or both object to compare are not persong object.");}
    }
  }

}//通过这个实例源码,可以总结这些内容:对于要进行排序的类对象(在这个源码中进行排序的是Person类的对象)在定义类的时候要继承IComparable接口,并实现其CompareTo方法以备提供一个默认的比较方式;对于高级一点的比较方式,则需要建立一个继承与IComparer的类(在这个源码中就是PersonCompareName),并在类中实现Compare方法
-----------------------------------------------------------
System.IO.File.Exists(path);//判断文件是否存在
-----------------------------------------------------------