您的位置:首页 > 编程语言 > C#

C#学习笔记(二)---在C#中创建类(上)

2017-05-15 12:10 127 查看

在C#中创建类(上)

在这一章中,我们将讨论类及类的成员。

是最常用的一种引用类型。

字段

字段是类或结构体中的变量。



只读修饰符:只读修饰符防止在构造后(类)被修改,只读修饰符只能在声明时或由构造函数传入时被初始化。

初始化字段:字段不用必须被初始化。没有赋初始值的字段会被赋予字段类型的初始值,如int的初始值是0等。

同时声明多个字段:可以用逗号在声明的时候来创建多个字段。

方法

方法使用一组语句实现某个行为。方法能从调用语句接收某种特定类型的输入参数,并以某种特定类型的输出结果来反馈给调用语句。void表示不向调用方返回任何值。此外,方法还可以用ref和out来想调用方返回值。

方法的签名在整个类中必须是唯一的,签名包括方法的名称、方法的参数类型等。ref和out也是方法签名的一部分。

方法可以使用以下修饰符:



重载方法:多个方法公用一个方法名、参数上的不同可造成方法的重载。注意重载和重写(override)的不同,重写是在类的继承上,通过virtual和override来完成的。

值传递和引用传递:它们也是方法签名的一部分。需要注意的是,Foo(ref int x)和Foo(out int x)不能同时出现在同一个类中。

类实例构造方法:构造方法执行类或构造体初始化的行为(代码),构造方法的定义和方法相似,不同的只是构造方法的名称和返回值只能(必须)是类本身(封装它的类)。构造方法接收修饰符:



类或者结构体的构造方法可以重载。为了避免重复编码,一个构造函数可以调用另一个构造方法。

using System;
public class Wine
{
public decimal Price;
public int Year;
public Wine (decimal price) { Price = price; }
public Wine (decimal price, int year) : this (price) { Year = year; }
}


上例表示一个构造函数调用另一个构造函数使用的关键字是:this。通常的调用法则是用参数列表少的构造函数调用参数列表多的构造函数(c#本质论里有讲)。被调用的构造函数先执行。C#为没有定义构造方法的类默认生成一个无参的构造方法。

构造方法和字段的初始化顺序:先按字段的声明顺序来执行字段的初始化,然后执行构造方法。

非公有构造方法:构造方法不一定是共有的,通常,定义非公有的构造函数主要是为了在一个静态的方法中控制类实例的创建:

public class Class1
{
Class1() {} // Private constructor
public static Class1 Create (...)
{
// 在这里定义自己的逻辑,用于返回类的实例
...
}
}


静态方法可以用于从池中返回类对象,而不必创建一个新的实例。或用来根据不同的输入属性来返回不同的子类。

对象初始化器:为了简化创建类实例的过程,可以在调用构造方法的后面直接初始化类的可访问字段和属性。初始化器是C#3.0引入的概念。

如果不用对象初始化器,也让构造方法接收可选参数:

public class shit
{
public string name { get; set; }
public string age { get; set; }

public shit(string name,string age="18")
{
this.name = name;
this.age = age;
}
}
class Program
{
static void Main(string[] args)
{
shit shi2t=new shit("you","20");//"20"可以指定,也可以不指定(默认18)
Console.WriteLine(shi2t.age);
Console.ReadKey();
}
}


属性

从外表看属性和字段很相似,但是属性内部可以想方法一样包含逻辑。属性和字段的声明方式很相似,但是属性包含了Get块和Set块。读取属性时使用Get块,返回属性类型的值,给属性赋值时,使用Set块,它有一个命名为value的隐含参数,类型和属性类型相同,值直接被指定给后备字段(back-field)。

自动属性:

public string Shit{get;set;}


类似于上述的格式的属性叫做自动属性,编译器会在后台自动生成一个后备字段,对属性进行支持。如果对外希望属性暴露成只读的,可以将属性设置为private set。注意:属性本身具有较高的访问权限,(本例中是public)然后在如要设置较低级别的访问器上添加较低级别的访问修饰符。

C#属性被编译成Get_xxx和Set_xxx的方法。

索引器

索引器暂时先不写了,没特么怎么用过。

常量

常量是在编译时已知并在程序的生存期内不发生更改的不可变值。常量使用 const 修饰符进行声明。只有 C# 内置类型(System.Object 除外)可以声明为 const。

用户定义的类型(包括类、结构和数组)不能为 const。请使用 readonly 修饰符创建在运行时初始化一次即不可再更改的类、结构或数组。

C# 不支持 const 方法、属性或事件。

可以使用枚举类型为整数内置类型(例如 int、uint、long 等等)定义命名常量。

常量必须在声明时初始化。例如:

public const int months = 12;


在此示例中,常量 months 始终为 12,不可更改,即使是该类自身也不能更改它。实际上,当编译器遇到 C# 源代码(例如 months)中的常量修饰符时,将直接把文本值替换到它生成的中间语言 (IL) 代码中。因为在运行时没有与常量关联的变量地址,所以 const 字段不能通过引用传递,并且不能在表达式中作为左值出现。

System_CAPS_note注意

当引用在其他代码如 DLL 中定义的常量值时应十分谨慎。如果新版本的 DLL 为常量定义了新的值,程序仍将保留旧的文本值,直到针对新版本重新编译程序。

可以同时声明多个相同类型的常量,例如:

class Calendar2
{
const int months = 12, weeks = 52, days = 365;
}


因为常量值对该类型的所有实例是相同的,所以常量被当作 static 字段一样访问。不使用 static 关键字声明常量。未包含在定义常量的类中的表达式必须使用类名、一个句点和常量名来访问该常量。例如:

int birthstones = Calendar.months;


常量也可以在方法内部声明,像局部变量一样。

静态构造方法

静态构造方法是每个类执行一次,不是每个类实例执行一次。一个类有且只有一个静态构造方法,而且必须没有参数,必须和类名同名。

运行时在使用类之前调用静态构造方法,下面两种情况可以触发静态构造方法:

①实例化类

②访问类的静态成员

静态构造方法只有两个修饰符:unsafe、extern

如果静态构造方法抛出一个未处理异常,则类在应用程序的整个生命周期都是不可用的。

静态构造函数主要用于初始化类的静态成员。

静态构造函数和字段初始化顺序:静态字段初始化器在调用静态构造方法之前运行,如果一个类没有静态构造函数,字段初始化器将在类使用前执行—-或者在运行时的任意一个更早的时间执行。这表明静态构造方法的存在可能使字段的初始化比正常时间晚执行。

静态字段按其声明的顺序进行初始化:

public static int x=y;//0
puvlic static int y=3;//3


如果调用x和y,可以发现x的值是0,y的值是3。

静态类

类可以被标记为static,这表明它必须由静态成员组成,并且不能产生子类。System.Console和System.Math就是静态类的最好示例。

终止器(Finalizers)

终止器是只能在类中使用的方法,其在对象被GC回收之前执行(废话)。终止器的语法是:

class Class1
{
~Class1()
{
...
}
}


这实际上是重载对象的Finalize方法的C#语法,编译器将其扩展成如下的方法声明:

protected override void Finalize()
{
...
base.Finalize();
}


终止器允许使用unsafe修饰符。

分布类和分布方法

以后再写

继承

为了扩展或自定义原类,类可以继承,继承可以让你重用另一个类的方法,而无需重新构建。一个类只能继承自唯一的类,但是可以被多个类继承,从而形成类的层次。

多态

引用是多态的,意味着x类型的变量可以指向y类型的实例。多态之所以能够实现,是因为子类拥有基类的所有特征,反过来,则不正确。例如,一个Display(子类)方法不能接收一个Display(基类)的参数。关于这个说明,有如下比喻更好理解:子类继承自基类,也就是扩展了基类,那么对于子类来说,子类拥有的内容通常要比基类多,也就是说子类的“视角”要宽于基类,如果要在一个需要子类类型的参数中放入一个基类,那么,由于基类的视角要“窄”于子类,那么,有一些东西基类就“看不到”,所以,编译器会认为是错误。

对于多态性,这里需要加一点的是virtual标记的虚方法,“运行时”调用虚方法派生得最远的实现。:

interface IProgram
{
void Fun();
}
class AProgram : IProgram
{
public virtual void AFun()
{
Console.WriteLine("AProgram.AFun");
}
void IProgram.Fun()
{
AFun();
}
}
class Program:AProgram
{
public new void AFun()
{
Console.WriteLine("Program.AFun");
}
staticvoid Main(string[] args)
{
Program pro =new Program();
((IProgram)pro).Fun();
pro.AFun();
Console.Read();
}
}


上述代码关系如下:IProgram接口定义了方法,在AProgram类中实现了该接口,Program中又继承了AProgram(也必须实现IProgram接口)AProgram中定义的实现接口的方法被定义为virtual,而在Program中又将该方法修饰为new(隐藏),则通过下面的代码来调用:

Program pro =new Program();
((IProgram)pro).Fun();
pro.AFun();
Console.Read();


结果是:

①((IProgram)pro).Fun();

//“AProgram.AFun”,虚方法的调用满足继承链上virtual的最远的。

②pro.AFun();

//“Program.AFun”

类型转换和引用转换

对象的引用可以被:

①安全的(隐式的)向上转换成基类的引用;

②不安全的(显示的)向下转换成子类的引用。如下伪代码:

子类 subClass=new 子类();
基类 a=subClass;
Console.WriteLine(a==subClass);//true
Console.WriteLine(a.子类的方法());//错误,访问不到。


虽然基类和子类的引用都指向了子类的对象,但是对于基类引用来说,有更严格的限制:基类的引用是不能够访问到子类“独有”的成员的。这就是上面比喻所说的“视角”问题。

as运算符:as运算符在向下转换出错时为变量赋值null而不抛出异常。下面的例子:

public class baseClass//基类
{
public baseClass()
{
Console.WriteLine("baseClass");
}
}

public class subClass : baseClass//子类
{
public subClass()
{
Console.WriteLine("subClass");
}
}
static void Main(string[] args)
{
baseClass baseC=new baseClass();//打印“base Class”
subClass sub=new subClass();//打印“baseClass和subClass”
sub=baseC as subClass;
Console.WriteLine(sub==null?"null":"not null");//打印“null”
Console.ReadKey();
}


上面这段代码说明2个问题:

子类实例化时要先运行基类的构造方法

as操作符执行向下转换时,如果转换失败会将变量赋值为null

接着执行判断变量是否为null,如果不需要判断变量是否为null的话,用cast(括号那种格式的)更好,因为会抛出异常来让我们了解程序到底在什么地方出现了问题。**提示**as运算符不能用来实现自定义转换,以及值类型转换。as可以用来向上转换,但这通常是多余的,因为向上转换隐式的就能实现。

is运算符:这个运算符要执行的转换是否能成功,它检查一个对象是否是从另一个类型派生或者实现了某个接口,所以,它通常在执行向下转换前执行。同样,is运算符不能用于自定义类型和值类型的转换,但是可以用于拆箱机制的类型转换。

virtual:该关键字可以让子类重写(配合override)。方法、属性、事件、索引器都可以virtual。virtual方法和override方法的标志、返回值、和访问权限必须一致。可以通过base关键字实现对基类成员的访问(在override方法中)。

抽象类和抽象成员

abstract被生命为abstract的类称为抽象类,抽象类可以包含抽象成员和虚成员(virtual),抽象成员与virtual成员不同之处在于抽象成员不提供具体实现,具体实现由子类提供(除非子类也是abstract)。抽象类不能被实例化。

隐藏成员

隐藏继承成员:基类和子类可以定义相同的成员。但有的时候需要故意隐藏基类的成员的时候,可以在子类中用new修饰。该修饰符仅仅是告诉编译器不要发出警告:



修饰符new把你的意图传达给编译器和其他编程人员,告诉它们你不是无意的。

参考下例:

public class baseClass
{
public int x=10;
public baseClass()
{
Console.WriteLine("baseClass");
}
}
public class subClass : baseClass
{
public new  int x=11;
public subClass()
{
Console.WriteLine("subClass");
}
}
static void Main(string[] args)
{

subClass sub=new subClass();//打印baseClass和subClass
baseClass    baseC = sub;//不打印
Console.WriteLine(baseC.x);//10
Console.WriteLine(sub.x);//11
Console.ReadKey();
}


new和virtual的比较

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();


密封方法和类

密封方法和类:重写的方法可以使用sealed关键字进行修饰,以防止更深层次的子类对它进行重载。也可以在类上面使用sealed,可以使该类不能被继承,从另一个层面上来说,也使该类中的所有方法被“密封”了。

base关键字

base关键字:base和this很类似,他有两个重要的目的:

①从子类中访问重写的方法成员;

②调用基类的构造方法;

构造和继承:如果基类和子类都没有自己的构造方法,则编译器会自动在其需要实例化的时候加上一个无参的构造方法用于实例化,但是,如果基类有自己自定义的构造方法,并且有参数,则子类必须声明自己的构造方法。并用base来调用基类的构造方法,这样做的用途在于基类的构造方法总是先于子类的构造方法,base关键字可以保证基类的构造方法可以被顺利调用。

构造方法和初始化执行的顺序:当对象初始化时,按以下顺序进行初始化:

①子类中的字段初始化

②指定基类构造方法中的变量,对该变量进行评估

③基类中的字段进行初始化

④基类的构造方法执行

⑤子类构造方法的方法体执行(开始初始化)

代码如下:

public class B
{
int x = 1; // Executes 3rd
public B (int x)
{
... // Executes 4th
}
}
public class D : B
{
int y = 1; // Executes 1st
public D (int x)
: base (x + 1) // Executes 2nd
{
... // Executes 5th
}
}


重载和解析

继承对方法的重载有很大的影响。下面两个重载:

static void Foo(asset a){};
static vid Foo(House b){};


当重载被调用时,类型最明确的被优先调用。

House a =new House();
Foo(a);//调用Foo(House)


具体调用那个是静态决定的(编译时决定的)不是在运行时。(这里又让我想到了一句话:C#是静态的,意即C#的类型在编译时就是已经确定的。这和dynamic是不同的)如下面例子:

Asset a=new House();
Foo(a);//调用的是Foo(Asset)


上面的例子调用的就是在编译时确定的类型Asset的Foo(Asset)方法,而不是在运行时确定的类型House。

提示:如果是用dynamic关键字修饰的
dynamic a=new House();
那么决定到底调用那个重载方法将在运行时决定。

object类型

obejct是所有C#中所有东西的最终基类。任何类型都可以向上转型为object类型。承载了类的优点,object是引用类型,但是值类型如int也能够和object进行相互转换。并加入栈中,这个性质被C#称为类型一致化。当值类型和object进行转换时,CLR必须做一些特定的工作,实现值类型和引用类型之间的转换的过程称为装箱拆箱

装箱是值类型转换为引用类型的过程。拆箱是引用类型转换为值类型的过程,拆箱转换必须显式的进行。装箱和拆箱的实质就是赋值,装箱转换是将值类型的实例赋值到引用类型的对象中,而拆箱转换是将对象的内容赋值回值类型的实例中。

静态和运行时类型检查

C#在静态(编译时)和运行时都会进行类型检查。静态检查能够在程序没有运行的情况下进行检查正确性。

通过引用的向下转换以及拆箱转换时将由CLR执行运行时检查。之所以可以进行运行时检查,是因为对象内部存储了对象的类型标识,这个标志可以通过object的GetType方法进行提取。

GetType方法和typeof运算符

所有的C#类型在运行时都会维护System.Type的实例。由两个基本的方法可以得到System.Type的对象:

①在类实例上调用GetType方法

②在类名上调用typeof

GetType在运行时赋值,typeof在编译时静态赋值(如果使用泛型类型,那么它将由即时编译器解析)

System.Type有针对类型名、程序集、基类的属性:

using System;
public class Point { public int X, Y; }
class Test
{
static void Main()
{
Point p = new Point();
Console.WriteLine (p.GetType().Name); // Point
Console.WriteLine (typeof (Point).Name); // Point
Console.WriteLine (p.GetType() == typeof(Point)); // True
Console.WriteLine (p.X.GetType().Name); // Int32
Console.WriteLine (p.Y.GetType().FullName); // System.Int32
}
}


ToString方法

ToString方法返回类型的默认文本表述,这个方法可以被重写。在程序开发的过程中可以体会到这个方法有很打的用处。当在值类型的对象上调用ToString方法时不会发生装箱。

结构体

以后再写

访问权限修饰符



//Class2可在它所在的程序集外部进行方位; Class1不能:
class Class1 {} // Class1是internal (默认的)
public class Class2 {}
//ClassB在同一个程序集中将字段x暴露给其他类; ClassA没有:
class ClassA { int x; } // x is private (default)
class ClassB { internal int x; }
//子类调用BaseClass的函数中,可以调用Bar,不可以调用Foo()
class BaseClass
{
void Foo() {} // Foo is private (default)
protected void Bar() {}
}
class Subclass : BaseClass
{
void Test1() { Foo(); } // Error - cannot access Foo
void Test2() { Bar(); } // OK
}


友元程序集

System.Runtime.CompilerServices.InternalsVisibleTo暂时没有涉及到,以后再说

程序集的权限封顶

类的权限是它内部声明成员访问权限的封顶,关于封顶权限最常见的例子是internal的class中的public成员:

class C
{
public void Foo()
{
......
}
}


类C的(默认)访问权限是internal,他作为Foo的最高访问权限,Foo的实际(有效)的权限是internal,而把Foo设置为public的一般原因为如果类C的访问权限要改成public,则重构起来更方便容易。

本章完

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: