一、枚举类型
枚举类型也成为枚举,它是一种创建数值类型的机制,这种值类型的可能取值是预定义的,而对于其中的每个可能取值,都有一个有意义的名称。这看似简单,但实际上枚举类型功能强大。通过定义一组有效值,而程序员能够理解表层含义。这样,代码的含义将不言自明,也不再模糊。
要定义枚举,必须在标识符前面加上关键字enum,然后在枚举体内定义一组有效值,并用逗号分隔它们。用作值名称的标识符必须遵循的规则与变量标识符相同。
ps:枚举值
最后一个枚举值后面的逗号是可选的,但最好不要省略,这样以后添加枚举值将更容易。
ps:多名称值
可以有多个名称对应于同一个数值,这在有多个名称表示同一个概念时很有用。为让多个名称对应于同一个数值,只需添加新名称,并将其设置成与另一个名称相等,如下所示。
枚举是一组只能为数值的命名常量,因此最好让每个名称对应于不同的数值。定义枚举时,编译器默认将第一个枚举值设置为整数零,其他值则依次加1。
ps:零值(zero value)
通常最好在枚举中包含对应于0的名称None。
枚举支持大多数可用于整数值的运算符,但并非所有这些运算符对枚举来说都有意义。对于枚举,执行得最多的操作是相等和不等测试。由于枚举属于值类型,因此也可声明可以为null的枚举。
ps:枚举的底层类型
枚举包含的所有值都必须是同一种数据类型的,这种数据类型称为底层类型(underlying type)。默认情况下,枚举的底层类型为 int,但是也可以使用任何预定义的整数类型:byte、short、int、long、sbyte、ushort、uint和ulong。
1.1 位标志枚举
通过使用位标志枚举(flags enumeration),可组合其中的值。使用位标志枚举时,可使用逻辑运算OR创建新的组合值。
为让位标志枚举的值能够组合,所有值都必须是2的幂。这是因为组合多个值时,必须能够确定结果为哪个离散值。因此,定义位标志枚举时,必须指定名称对应的值。
ps:Flags特性
常规枚举和位标志枚举之间的另一个差别是,后者需要使用Flags特性,它指定有关枚举的额外元数据。
Flags特性还改变了组合得到的枚举值的字符串表示(方法ToString返回的结果)。
虽然并非必须使用Flags特性,但是强烈建议这样做,因为向编译器和其他程序员清晰地表明了你的意图。
在简单枚举中,可以让名称None或最常见的默认名称对应于0,但是位标志枚举与此不同,它要求0对应于名称None,这个值意味着所有标志都未设置。
二、结构
在需要简单的用户定义的类型时,可将结构作为类的轻量级替代品。结构类似于类,可包含的成员类型与类相同,但是属于值类型而不是引用类型。结构与类的不同之处如下:
- 接口不支持继承。结构隐式地继承System.ValueType,而后者继承System.Object。就像类一样,结构也可继承接口
- 结构隐式地被密封,这意味着您不能继承结构
- 结构不能有析构函数,不能声明默认构造函数,也不能在结构体内初始化实例字段。如果结构提供了构造函数,就必须在其中给所有字段赋值
ps:基类库中的结构
除 string 和 object 外,所有基本数据类型都被实现为结构。.NET Framework提供了200多个公有结构,下面是一些常用的结构:
System.DateTime
System.DateTimeOffset
System.Guid
System.TimeSpan
System.Drawing.Color
System.Drawing.Point
System.Drawing.Rectangle
System.Drawing.Size
在C#中,结构的声明方法与类相同,只是需要使用关键字struct代替关键字class
2.1 方法
就像类可以定义方法一样,结构也可以。这些方法要么是静态方法,要么是实例方法,但是结构较常使用静态共有方法和私有实例方法
运算符重载
由于结构是用户定义的值类型,因此如果变量的类型为您定义的结构,就不能将大多数常见的运算符用于它。这是一种重大缺陷,所幸的是,C #通过运算符重载提供了一种解决这个问题的方式。
如果将运算符视为名称特殊的方法,那么运算符重载就是一种特殊的方法重载。要声明重载的运算符,可定义一个public static方法,其名称为关键字operator和要重载的运算符的符号。另外,至少要有一个参数的类型与重载运算符所属的类型相同。下表列出了可重载的运算符。
类别 | 运算符 |
---|---|
单目 | + - ! ~ ++ true false |
乘除 | * / % |
加减 | + - |
移位 | << >> |
关系 | < > <= >= |
逻辑 | & 丨 ^ |
相等性 | == != |
ps:语言互操作性
并非所有.NET 语言都支持运算符重载,因此如果创建的类要在其他语言中使用,它们应符合CLS,并提供与定义的重载运算符对应的替代品。
通常,应成组地重载运算符。例如,如果重载了相等运算符,也应重载不等运算符。对于这个指导原则,唯一的例外是求补运算符(~)和逻辑非运算符(!)。下表列出了应同时重载的成组运算符。
转换运算符
在用户定义的结构中,可重载运算符以便能够对定义的数据执行常见的运算,同样,也可创建重载的转换运算符,以影响强制转换和转换过程。同样,如果将转换和强制转换视为名称特殊的函数,则转换重载也是一种特殊的方法重载。
ps:显示转换和隐式转换
隐式转换是属于扩大(widening)转换,因为原始值不会因转换而丢失数据。显式转换属于缩小(Narrowing)转换,因为原始值可能因转换而丢失数据。
定义自己的转换运算符时,应牢记这些行为。如果定义的转换可能丢失数据,应将其定义为显式转换;如果定义的转换是安全的,即不会丢失数据,就应将其定义为隐式转换。
内置数据类型支持隐式转换和显式转换,其中隐式转换不需要特殊语法,但显式转换需要。对于自己定义的类型,可重载这些显式转换和隐式转换,方法是声明自己的转换运算符,其规则与声明运算符重载类似。
要声明转换运算符,可定义一个 public static方法,其名称为关键字 operator,返回类型为要转换到的类型。转换运算符只接受一个参数,那就是要转换的类型。
如果要声明隐式转换,就可在关键字operator前面加上implicit;否则,加上关键字explicit。有时结合使用转换运算符和运算符重载,以减少要定义的运算符重载。
2.2 构造和初始化
就像必须给对象指定初始状态一样,结构也如此。对于对象,这是通过构造函数完成的,但结构是值类型,无需调用构造函数就可以创建结构变量。例如,可像下面这样创建一个NumberStruct变量:
NumberStruct ns1;
上述代码创建了一个新变量,但是字段处于未初始化状态;如果此时试图访问字段,将发生编译错误。通过调用构造函数,可确保字段被初始化了。
结构初始化的另一个方面是,不能将未完全初始化的结构变量赋给另一个结构变量,即不能将这样的变量放在赋值运算符右边。这意味着下面的代码合法:
NumberStruct ns1 = new NumberStruct();
NumberStruct ns2 = ns1;
但下面的代码非法:
NumberStruct ns1;
NumberStruct ns2 = ns1;
ps:自定义默认构造函数
不同于类,结构不能有自定义的默认构造函数,也不能在构造函数外面初始化结构。因此,创建结构时,所有字段都被初始化为零值。
可以提供重载的构造函数,并利用构造函数串接。然而,当提供重载的构造函数时,必须初始化所有字段,这可在该构造函数中显式地进行,也可通过串接构造函数隐式地完成。
有趣的是,如果未显式初始化的字段都可接受零值,就可串接默认构造函数。
struct NumberStruct
{
public int Value;
}
class NumberClass
{
public int value = 0;
}
class Test
{
static void Main()
{
NumberStruct ns1 = new NumberStruct ();
NumberStruct ns2 = ns1;
ns2.Value = 42;
NumberClass nc1 = new NumberClass ();
NumberClass nc2 = nc1;
nc2.value = 42;
Console.WriteLine ("Struct:{0),{1}", ns1.Value, ns2.Value);
Console.WriteLine ("Class:{0),{1}", nc1.Value, nc2.Value);
}
}
由于ns1和ns2都是值类型NumberStruct,它们有各自的存储空间,因此给ns2.Number赋值不会影响ns1.Number的值。然而,由于nc1和nc2都是引用类型,并且指向同一个存储位置,因此给nc2.Number赋值将影响nc1.Number的值。
ps:使用属性还是公有字段
对于结构应使用属性还是公有字段存在一些争议。有些人认为总是应该使用属性,即使是在结构这样的简单类型中;而有些人认为,在结构中使用公有字段是能够接受的。
虽然使用公有字段更容易,但是这导致值类型是可以修改的,而通常不希望这样。定义自己的结构时,别忘了它们是值类型,应像字符串一样是不可修改的。为此,应提供可用于设置私有字段的构造函数,并提供只读属性用于获取私有字段的值。
附:C#中一些易混淆概念——构造函数、this关键字、部分类、枚举
1.构造函数
我们先创建一个类,如下面的代码:
class Program
{
static void Main(string[] args)
{
}
}
//创建一个Person类
class Person
{
}
然后生成代码。
我们使用.NET Reflector反编译该程序集。会发现该类一被编译,CLR会自动的为该类创建一个默认的构造函数。如下图:
所以在创建该对象的时候,会默认的为该类生成一个无参数的空方法体的构造函数。如果我们不显式的写明构造函数,CLR会为我们调用默认的构造函数。
class Person
{
//声明有实现的构造函数
public Person()
{
Console.WriteLine("我是超人!");
}
}
再次反编译该程序集,会发现添加的构造函数覆盖了C#编译器默认为该类生成的构造函数,如下图:
所以,当程序员手动添加了任意类型的构造函数,C#编译器就不会为该类添加默认的构造函数。
构造函数的特点:
①访问修饰符一般是Public②没有返回值,方法名与类名称一致;
2.This关键字的作用
①this关键字代表当前对象,当前运行在内存中的那一个对象。我们添加如下的代码:
private int nAge;
public int NAge
{
get { return nAge; }
set { nAge = value; }
}
//声明有实现的构造函数
public Person()
{
this.NAge = 100;
Console.WriteLine("我是超人!");
}
这时候我们反编译该程序集,会看到如下结果:
可以看到this关键字代替的就是当前的Person对象。
②this关键字后面跟“:”符号,可以调用其它的构造函数
我们再添加如下的代码:
#region 对象的构造函数
//声明有实现的构造函数
public Person()
{
this.NAge = 100;
Console.WriteLine("我是超人!");
}
public Person(int nAge)
{
Console.WriteLine("超人的年龄{0}", nAge);
}
//使用this关键字调用了第二个一个参数的构造函数
public Person(int nAge, string strName)
: this(1)
{
Console.WriteLine("我是叫{0}的超人,年龄{1}", strName, nAge);
}
#endregion
我们创建该对象看看是否调用成功。在Main函数中添加如下代码:
Person p = new Person(10,"强子");
我们运行代码,看到的打印结果如下:
由结果我们可以分析出,当含有两个默认参数的对象创建的时候应该先调用了一个参数的构造函数对对象进行初始化,然后有调用了含有两个参数的构造函数对对象进行初始化。
那么到底是不是这个样子呢?看下边的调试过程:
通过上面的调试过程我们会发现,当构造函数使用this关键字调用其它的构造函数时,首先调用的是该调用的构造函数,在调用被调用的构造函数,先执行被调用的构造函数,在执行直接调用的构造函数。
为什么要这个顺序执行?因为我们默认的传值是10,我们需要打印的超人的年龄是“10”,如果先执行直接调用的构造函数,就会被被调用构造函数覆盖。
3.部分类
在同一命名空间下可以使用partial关键字声明相同名称的类(同一命名空间下默认不允许出现相同的类名称),叫做部分类或者伙伴类。
如下图,当在同一命名空间下声明相同名称的类,编译器报错:
当我们使用Partial关键字时,可以顺利编译通过,如下图:
分别添加如下的代码:
partial class Person
{
private string strAddress;
public string StrAddress
{
get { return strAddress; }
set { strAddress = value; }
}
private string strNumber;
public string StrNumber
{
get { return strNumber; }
set { strNumber = value; }
}
public void Run()
{
}
}
partial class Person
{
#region 对象属性
private int nAge;
public int NAge
{
get { return nAge; }
set { nAge = value; }
}
private string strName;
public string StrName
{
get { return strName; }
set { strName = value; }
}
#endregion
#region 对象的构造函数
//声明有实现的构造函数
public Person()
{
this.NAge = 100;
Console.WriteLine("我是超人!");
}
public Person(int nAge)
{
Console.WriteLine("超人的年龄{0}", nAge);
}
public Person(int nAge, string strName)
: this(1)
{
Console.WriteLine("我是叫{0}的超人,年龄{1}", strName, nAge);
}
#endregion
public void Sing()
{
}
}
我们再次反编译该程序集,会发现如下的结果:
我们会发现使用Partial关键字的两个同名类,被编译成了同一个类。
所以部分类的特点:
①必须在同一个命名空间下的使用Partial关键字的同名类
②部分类其实就是一个类,C#编译器会把它们编译成一个类
③在一个伙伴类中定义的变量可以在另一个伙伴类中访问(因为他们就是一个类)。
4.Const关键字和Readonly关键字的区别
1)const关键字
在Main函数中添加如下的代码:
const string strName = "强子";
Console.WriteLine("我的名字叫{0}",strName);
编译过后,我反编译该程序集发现如下结果:
发现定义的常量并没有出现在反编译的代码中,而且使用Const常量的地方被常量代替了。
2)readonly关键字
添加如下代码:
class cat
{
readonly string reOnlyName = "强子";
public cat()
{
Console.WriteLine(reOnlyName);
}
}
生成后反编译该程序集发现,如下结果:
我们发现被readonly修饰的变量并没有被赋值,这是什么回事呢?我们点击cat类的构造函数时,看到如下结果:
我们发现被readonly修饰的变量是在被调用的时候赋值的。
那么被readonly修饰的变量的是就是不可变的么?当然不是,由反编译的结果我们知道,readonly修饰的变量是在被调用的时候在构造函数中被赋值的,那么我们可以在构造函数中修改readonly的默认值
添加如下代码:
class cat
{
readonly string reOnlyName = "强子";
public cat()
{
this.reOnlyName = "子强";
Console.WriteLine(reOnlyName);
}
}
在Main()函数中添加如下的代码:
cat ct = new cat();
运行结果如下:
说明我们成功在构造函数中修改了readonly变量的值。
readonly和const的区别:
const常量在声明的时候就必须赋初始值,这样声明变量可以提高程序的运行效率。而readonly变量声明时可以不赋初始值,但一定要早构造函数中赋初始值。
也就是说,const变量在编译的时候就要确定常量的值,而readonly是在运行的时候确定该变量的值的。
5.解析枚举
枚举的级别和类的级别一样,可以自定义数据类型,可以在枚举名称后使用“:”来指明枚举类型。看如下代码:
//定义一个方向的枚举类型,枚举成员使用","分割
enum Direction:string
{
east,
west,
south,
north
}
编译会报错,错误信息如下:
由此我们可以知道枚举的数据类型是值类型。
因为枚举是数据类型,所以可以直接声明访问,如下代码:
class Program
{
static void Main(string[] args)
{
//枚举是数据类型可以直接声明
Direction dr = Direction.east;
Console.WriteLine(dr);
Console.ReadKey();
}
}
//定义一个方向的枚举类型,枚举成员使用","分割
enum Direction
{
east,
west,
south,
north
}
也可以这样访问枚举类型
class Program
{
static void Main(string[] args)
{
//枚举是数据类型可以直接声明
// Direction dr = Direction.east;
Person p=new Person();
//直接调用枚举变量
p.dir = Direction.east;
Console.WriteLine(p.dir);
Console.ReadKey();
}
}
class Person
{
private string strName;
//直接声明枚举变量
public Direction dir;
}
每一个枚举成员都对应了一个整型的数值,这个数值默认从0开始递增,可以通过强制转换获取该枚举所代表的值。可以通过如下的代码访问:
Direction dr = Direction.east;
int i = (int)dr;
我们还可以手动为每一个枚举成员赋值,代表的是整型数值,赋值后该枚举成员所代表的值就是所赋的值。如下代码:
enum Direction
{
east=1,
west=0,
south=2,
north=3
}
将字符串转换成枚举
string strDir = "east";
//将字符串转换成枚举类型
Direction d1=(Direction)Enum.Parse(typeof(Direction),strDir);
//转换的时候忽略大小写
Direction d2 = (Direction)Enum.Parse(typeof(Direction), strDir,true);
最后我们再来探究一个空指针异常的问题
首先我们先声明一个Dog类:
class Dog
{
private int nAge;
public int NAge
{
get { return nAge; }
set { nAge = value; }
}
private string strName;
public string StrName
{
get { return strName; }
set { strName = value; }
}
}
在Main()函数中我们这样调用
Dog d = null;
d.StrName = "旺旺";
结果会报错,如下图
我们已经为属性,封装字段了,但是为什么没有办法给字段赋值呢?我们就来探究一下这个问题。
当我们实例化Dog对象,即
Dog d = new Dog();
.NET Framwork做了什么工作呢?如下图:
那为什么会报错呢,原因如下图: