欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

深入探索《C# 7.0核心技术指南》:第二部分 C#语言基础知识解析

最编程 2024-08-10 16:42:26
...

点击查看第一章

第2章

C#语言基础
本章将介绍一些C#语言的基础知识。
本章和接下来的两章中的所有程序和代码片段都可以作为交互式示例在LINQPad中运行。阅读本书时使用这些示例可以加快你的学习进度。在LINQPad中编辑执行这些示例可以立即看到结果,无须在Visual Studio中建立项目和解决方案。
若要下载这些示例,请点击LINQPad中的Samples选项卡,然后点击“Download more samples”。LINQPad是免费程序,详见http://www.linqpad.net

2.1 第一个C#程序

以下程序计算12乘以30,并将结果360打印到屏幕上。双斜线“//”表示其后的内容是注释:

using System;                     // Importing namespace

class Test                        // Class declaration
{
  static void Main()              // Method declaration
  {
    int x = 12 * 30;              // Statement 1
    Console.WriteLine (x);        // Statement 2
  }                               // End of method
}                                 // End of class

该程序的核心是以下两个语句:

int x = 12 * 30;
Console.WriteLine (x);

在C#中,语句按顺序执行,每个语句都以分号(或者代码块,详见本章后续内容)结尾。第一个语句计算表达式12*30的值,并把结果存储到一个局部变量x中,该变量是一个整数类型。第二个语句调用Console类的WriteLine方法,将变量x的值输出到屏幕上的文本窗口中。
方法(method)是由一系列语句(语句块)组成的行为。语句块由一对大括号,及其中的零个或者多个语句组成。示例中定义了一个名为Main的方法:

static void Main()
{
  ...
}

编写高层函数来调用低层函数可令程序得到简化。可重构(refactor)该程序,使用一个可重用的方法来计算某个整数乘以12的结果:

using System;

class Test
{
  static void Main()
  {
    Console.WriteLine (FeetToInches (30));      // 360
    Console.WriteLine (FeetToInches (100));     // 1200
  }

  static int FeetToInches (int feet)
  {
    int inches = feet * 12;
    return inches;
  }
}

方法可以通过参数来接受调用者输入的数据,并通过指定的返回类型向调用者返回输出数据。上述代码中定义了一个FeetToInches方法,该方法有一个用于输入英尺的参数和一个用于输出英寸的返回类型:

static int FeetToInches (int feet ) {...}

示例中的字面量30和100是传递给FeedToInches方法的实际参数(argument)。而Main方法后的括号中是空的,因而没有任何参数。其返回类型是void说明它不向调用者返回任何值:

static void Main()

C#将Main方法作为程序执行的默认入口点。Main方法也可以返回整数值(而非void)从而将其返回给程序的执行环境(非0返回值往往代表一个错误)。Main方法还可以接受一个字符串数组作为参数(数组中包含了传递给可执行程序的任何实际参数)。例如:

static int Main (string[] args) {...}

数组(例如string[])是固定数量的某种特定类型元素的集合。数组由元素类型和它后面的方括号指定。相关内容将在2.7节介绍。
方法是C#中的诸多种类的函数之一。另一种函数是我们用来执行乘法运算的*运算符。其他的函数种类还包括构造器、属性、事件、索引器和终结器。
本例将两个方法组合到一个类中。类由函数成员和数据成员组成,并形成面向对象的构件块。Console类将处理命令行输入/输出功能的成员,例如WriteLine方法,聚集在一起。Test类则由Main方法和FeetToInches两个方法组成。类是类型之一,我们将在2.3节中介绍它。
程序的最外层将类型组织到了命名空间中。为了使System命名空间在应用程序中生效,并能够使用Console类,需要使用using指令。应将所有的类定义在TestPrograms命名空间中,例如:

using System;

namespace TestPrograms
{
  class Test  {...}
  class Test2 {...}
}

.NET Framework由若干嵌套的命名空间组织而成。例如,以下命名空间中包含处理文本的类型:

using System.Text;

使用using指令仅仅是为了方便;也可以使用命名空间加类型名称(例如System.Text.StringBuilder)这种完整限定名称来引用类型。
编译
C#编译器将一系列.cs扩展名的源代码文件编译成程序集。程序集是.NET中的最小打包和部署单元。程序集可以是一个应用程序或者是一个库。普通的控制台程序或Windows应用程序是一个.exe文件,包含一个Main方法。而库是一个.dll文件,即一个没有入口点的.exe文件。库可以被应用程序或其他的库调用(引用)。.NET Framework就是由一系列库组成的。
C#编译器是csc.exe。我们既可以使用像Visual Studio这样的IDE来编译程序,也可以在命令行中手动调用csc命令编译C#程序(编译器本身通过库调用,详情参见第27章)。如需手动编译C#程序,首先将程序保存成文件(例如MyFirstProgram.cs),然后进入命令行并调用csc命令(csc位于%ProgramFiles(X86)%msbuild14.0bin)译注1,如下所示:

csc MyFirstProgram.cs

这个命令将生成名为MyFirstProgram.exe的应用程序。
奇怪的是,.NET Framework 4.6和4.7仍然包含C# 5的编译器。若要使用C# 7命令行编译器,必须安装Visual Studio 2017或MSBuild 15。
如需生成库(.dll),请使用如下命令:
csc /target:library MyFirstProgram.cs
我们将在第18章详细介绍程序集。

2.2 语法

C#的语法基于C和C++语法。在本节中,我们将使用下面的程序介绍C#的语法元素:

using System;

class Test
{
  static void Main()
  {
    int x = 12 * 30;
    Console.WriteLine (x);
  }
}

2.2.1 标识符和关键字

标识符是程序员为类、方法、变量等选择的名字。下面按顺序列出了上述示例中的标识符:
System Test Main x Console WriteLine
标识符必须是一个完整的词,它由以字母和下划线开头的Unicode字符构成。C#标识符是区分大小写的。通常约定参数、局部变量以及私有字段应该以小写字母开头(例如myVariable),而其他类型的标识符则应该以大写字母开头(例如MyMethod)。
关键字是对编译器有特殊意义的名字。以下是示例中用到的关键字:
using class static void int
大部分关键字是保留的,这意味着它们不能用作标识符。以下列出了C#的所有关键字:

image.png

2.2.1.1 避免冲突

如果希望用关键字作为标识符,需在关键字前面加上@前缀。例如:

class class  {...}      // Illegal
class @class {...}      // Legal

@并不是标识符的一部分,所以@myVariable和myVariable是一样的。
@前缀在调用使用其他拥有不同关键字的.NET语言编写的库时非常有用。

2.2.1.2 上下文关键字

一些关键字是上下文相关的,它们有时不用添加@前缀就可以用作标识符。它们是:

add            dynamic    in        orderby    var
ascending        equals        into        partial    when
async        from        join        remove        where
await        get        let        select        yield
by            global        nameof        set
descending        group        on        value

使用上下文关键字作为标识符时,应避免与上下文中的关键字混淆。

2.2.2 字面量、标点与运算符

字面量是静态的嵌入程序中的原始数据片段。上述示例中用到的字面量有12和30。
标点有助于划分程序结构。以下是示例中用到的标点:

{   }   ;

大括号可将多条语句形成一个语句块。
分号用于结束一条语句。(但语句块并不需要分号。)这意味着语句也可以放在多行中:

Console.WriteLine
  (1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10);

运算符用于改变和结合表达式。大多数C#运算符都以符号表示,例如乘法运算符*。我们将在本章后续内容中详细介绍运算符。在上述示例中出现的运算符有:

.  ()   *   =

点号(.)表示某个对象的成员(或者数字字面量的小数点)。括号在声明或调用方法时使用;空括号在方法没有参数时使用。(本章后续还会介绍括号的其他的用途。)等号用于赋值操作。(双等号==用于相等比较,请参见本章后续内容)。

2.2.3 注释

C#提供了两种方式源代码文档:单行注释和多行注释。单行注释由双斜线开始,到本行结束为止。例如:

int x = 3;   // Comment about assigning 3 to x

多行注释由/*开始,由*/结束。例如:

int x = 3;   /* This is a comment that
                spans two lines */

注释也可以嵌入XML文档标签中,我们将在4.17节中介绍。

2.3 类型基础

类型是值的蓝图。在以下示例中,我们使用了两个int类型的字面量12和30,并声明了一个int类型的变量x:

static void Main()
{
  int x = 12 * 30;
  Console.WriteLine (x);
}

变量表示一个存储位置,其中的值可能会不断变化。与之对应,常量总是表示同一个值(后面会详细介绍):

const int y = 360;

C#中的所有值都是某一种类型的实例。值或者变量所包含的可能取值均由其类型决定。

2.3.1 预定义类型示例

预定义类型是指那些由编译器特别支持的类型。int就是一种预定义类型,它代表一系列能够存储在32位内存中的整数集,其范围从-231到231-1,并且它是该范围内数字字面量的默认类型。我们能够对int类型的实例执行算术运算等功能:

int x = 12 * 30;

C#中的另一个预定义类型是string。string类型表示字符序列,例如“.NET”或者“http://oreilly.com” 。我们可以通过以下方式调用函数来操作字符串:

string message = "Hello world";
string upperMessage = message.ToUpper();
Console.WriteLine (upperMessage);               // HELLO WORLD

int x = 2015;
message = message + x.ToString();
Console.WriteLine (message);                    // Hello world2015

预定义类型bool只有两种值:true和false。bool类型通常与if语句一起控制条件分支执行流程。例如:

bool simpleVar = false;
if (simpleVar)
  Console.WriteLine ("This will not print");

int x = 5000;
bool lessThanAMile = x < 5280;
if (lessThanAMile)
  Console.WriteLine ("This will print");

在C#中,预定义类型(也称为内置类型)拥有相应的C#关键字。在.NET Framework中的System命名空间下也包含了很多不是预定义类型的重要类型(例如DateTime)。

2.3.2 自定义类型示例

我们能使用简单函数来构造复杂函数,同样也可以使用基元类型来构建复杂类型。以下示例定义了一个名为UnitConverter的自定义类型。这个类将作为单位转换的蓝图:

using System;

public class UnitConverter
{
  int ratio;                                                 // Field
  public UnitConverter (int unitRatio) {ratio = unitRatio; } // Constructor
  public int Convert   (int unit)    {return unit * ratio; } // Method
}

class Test
{
  static void Main()
  {
    UnitConverter feetToInchesConverter = new UnitConverter (12);
    UnitConverter milesToFeetConverter  = new UnitConverter (5280);

    Console.WriteLine (feetToInchesConverter.Convert(30));    // 360
    Console.WriteLine (feetToInchesConverter.Convert(100));   // 1200
    Console.WriteLine (feetToInchesConverter.Convert(
                         milesToFeetConverter.Convert(1)));   // 63360
  }
}

2.3.2.1 类型的成员

类型包含数据成员和函数成员。UnitConverter的数据成员是ratio字段,函数成员是Convert方法和UnitConverter的构造器。

2.3.2.2 预定义类型和自定义类型

C#的优点之一是其中的预定义类型和自定义类型非常相近。预定义int类型是整数的蓝图。它保存了32位的数据,提供像ToString这种函数成员来使用这些数据。类似地,我们自定义的UnitConverter类型也是单位转换的蓝图。它保存比率数据,还提供了函数成员来使用这些数据。

2.3.2.3 构造器和实例化

将类型实例化即可创建数据。预定义类型可以简单地通过字面量进行实例化,例如12或"Hello World"。而自定义类型则需要使用new运算符来创建实例。以下的语句创建并声明了一个UnitConverter类型的实例:

UnitConverter feetToInchesConverter = new UnitConverter (12);

使用new运算符后会立刻实例化一个对象,调用对象的构造器进行初始化。构造器的定义像方法一样,不同的是方法名和返回类型简化为所属的类型名称:

public class UnitConverter
{
  ...
  public UnitConverter (int unitRatio) { ratio = unitRatio; }
  ...
}

2.3.2.4 实例与静态成员

由类型的实例操作的数据成员和函数成员称为实例成员。UnitConverter的Convert方法和int的ToString方法就是实例成员的例子。在默认情况下,成员就是实例成员。
那些不是由类型的实例操作,而是由类型本身操作的数据成员和函数成员必须标记为static。Test.Main和Console.WriteLine就是静态方法。事实上,Console类是一个静态类,它的所有成员都是静态的。由于Console类型无法实例化,因此控制台将在整个应用程序内共享使用。
我们来对比实例成员和静态成员。在下面的代码中,实例字段Name属于特定的Panda实例,而Population则属于所有Panda实例:

public class Panda
{
  public string Name;             // Instance field
  public static int Population;   // Static field

  public Panda (string n)         // Constructor
  {
    Name = n;                     // Assign the instance field
    Population = Population + 1;  // Increment the static Population field
  }
}

下面的代码创建了两个Panda实例,先打印它们的名字,再打印总数:

using System;

class Test
{
  static void Main()
  {
    Panda p1 = new Panda ("Pan Dee");
    Panda p2 = new Panda ("Pan Dah");

    Console.WriteLine (p1.Name);      // Pan Dee
    Console.WriteLine (p2.Name);      // Pan Dah

    Console.WriteLine (Panda.Population);   // 2
  }
}

如果试图求p1.Population或者Panda.Name的值,则会生成一个编译时错误。

2.3.2.5 public关键字

public关键字将成员公开给其他类。在上述示例中,如果Panda类中的Name字段没有标记为公有(public)的,那么它就是私有的,且Test类就不能访问它。将成员标记为public就是类型的通信手段:“这就是我想让其他类型看到的,而其他的都是我私有的实现细节。”在面向对象的术语中,称之为类的公有成员封装了私有成员。

2.3.3 转换

C#可以转换兼容类型的实例。转换始终会根据一个已经存在的值创建一个新的值。转换可以是隐式或显式的:隐式转换自动发生而显式转换需要强制转换。在以下的示例中,我们把一个int隐式转换为long类型(其存储位数是int的两倍);并将一个int显式转换为一个short类型(其存储位数是int的一半):

int x = 12345;       // int is a 32-bit integer
long y = x;          // Implicit conversion to 64-bit integer
short z = (short)x;  // Explicit conversion to 16-bit integer

隐式转换只有在以下条件都满足时才能进行:
编译器能确保转换总能成功。
没有信息在转换过程中丢失。注1
相对地,只有在满足下列条件时才需要显式转换:
编译器不能保证转换总是成功。
信息在转换过程中有可能丢失。
(如果编译器可以确定某个转换一定会失败,那么这两种转换都无法执行。包含泛型的转换在特定情况下也会失败,请参见3.9.11节)
以上的数值转换是C#中内置的。C#还支持引用转换、装箱转换(见第3章)与自定义转换(请参见4.14节)。对于自定义转换,编译器并没有强制要求上述规则,因此没有良好设计的类型有可能在转换时出现意想不到的效果。

2.3.4 值类型与引用类型

所有的C#类型可以分为以下几类:

  • 值类型
  • 引用类型
  • 泛型参数
  • 指针类型

本节将介绍值类型和引用类型。泛型参数将在3.9节介绍,指针类型将在4.15节中介绍。
值类型包含大多数的内置类型(具体包括所有数值类型、char类型和bool类型)以及自定义的struct类型和enum类型。
引用类型包含所有的类、数组、委托和接口类型。(这其中包括了预定义的string类型。)
值类型和引用类型最根本的不同在于它们在内存中的处理方式。

2.3.4.1 值类型

值类型的变量或常量的内容仅仅是一个值。例如,内置的值类型int的内容是32位的数据。
可以通过struct关键字定义自定义值类型(参见图2-1):

image.png

public struct Point { public int X; public int Y; }

或采用更简短的形式:

public struct Point { public int X, Y; }

值类型实例的赋值总是会进行实例复制。例如:

static void Main()
{
  Point p1 = new Point();
  p1.X = 7;

  Point p2 = p1;             // Assignment causes copy

  Console.WriteLine (p1.X);  // 7
  Console.WriteLine (p2.X);  // 7

  p1.X = 9;                  // Change p1.X

  Console.WriteLine (p1.X);  // 9
  Console.WriteLine (p2.X);  // 7
}

图2-2中展示了p1和p2拥有不同的存储空间。

image.png

2.3.4.2 引用类型

引用类型比值类型复杂,它由两部分组成:对象和对象引用。引用类型变量或常量中的内容是一个含值对象的引用。以下示例将前面例子中的Point类型重新书写,令其成为一个类而非struct(请参见图2-3):

image.png

public class Point { public int X, Y; }

给引用类型变量赋值只会复制引用,而不是对象实例。这允许不同变量指向同一个对象,而值类型通常不会出现这种情况。如果Point是一个类,那么若重复之前的示例,则对p1的操作就会影响到p2了:

static void Main()
{
  Point p1 = new Point();
  p1.X = 7;

  Point p2 = p1;             // Copies p1 reference

  Console.WriteLine (p1.X);  // 7
  Console.WriteLine (p2.X);  // 7

  p1.X = 9;                  // Change p1.X

  Console.WriteLine (p1.X);  // 9
  Console.WriteLine (p2.X);  // 9
}

图2-4展示了p1和p2是指向同一对象的两个不同引用。

image.png

2.3.4.3 Null

引用可以赋值为字面量null,表示它并不指向任何对象:

class Point {...}
...

Point p = null;
Console.WriteLine (p == null);   // True

// The following line generates a runtime error
// (a NullReferenceException is thrown):
Console.WriteLine (p.X);

相对地,值类型通常不能有null的取值:

struct Point {...}
...

Point p = null;  // Compile-time error
int x = null;    // Compile-time error

C#中也有一种代表值类型为null的结构,称为可空(nullable)类型(请参见4.7节)。

2.3.4.4 存储开销

值类型实例占用的内存大小就是存储其字段所需的内存。例如,Point需要占用8字节的内存:

struct Point
{
  int x;  // 4 bytes
  int y;  // 4 bytes

}

从技术上说,CLR用整数倍字段的大小(最大到8字节)来分配内存地址。因此,下面的定义的对象实际上会占用16字节的内存(第一个字段的7个字节被“浪费了”):

struct A { byte b; long l; }

这种行为可以通过指定StructLayout属性来重写(请参见25.6节)。
引用类型要求为引用和对象单独分配存储空间。对象除占用了和字段一样的字节数外,还需要额外的管理空间开销。管理开销的精确值本质上属于.NET运行时实现的细节,但最少也需要8个字节来存储该对象的类型的键,以及一些诸如多线程锁的状态、是否可以被垃圾回收器固定等临时信息。根据.NET运行时是工作在32位抑或64位平台上,每一个对象的引用都需要额外的4到8个字节。

2.3.5 预定义类型分类

C#中的预定义类型有:
值类型

  • 数值

    • 有符号整数(sbyte、short、int、long)
    • 无符号整数(byte、ushort、uint、ulong)
    • 实数(float、double、decimal)
  • 逻辑值(bool)
  • 字符(char)

引用类型

  • 字符串(string)
  • 对象(object)

C#的预定义类型又称为框架类型,它们都在System命名空间下。下面的两个语句仅在拼写上有所不同:

int i = 5;
System.Int32 i = 5;

在CLR中,除了decimal之外的一系列预定义值类型属于基元类型。之所以将其称为基元类型是因为它们在编译过的代码中有直接的指令支持。而这种指令通常翻译为底层处理器直接支持的指令。例如:

// Underlying hexadecimal representation
int i = 7;         // 0x7
bool b = true;     // 0x1
char c = 'A';      // 0x41
float f = 0.5f;    // uses IEEE floating-point encoding

System.IntPtr以及System.UIntPtr类型也是基元类型(参见第25章)。

2.4 数值类型

表2-1中列出了C#中所有的预定义数值类型。

image.png
image.png

在整数类型中,int和long是最基本的类型,C#和运行时都对其有良好的支持。其他的整数类型通常用于实现互操作性或存储空间使用效率要求更高的情况。
在实数类型中,float和double称为浮点类型,注2并通常用于科学和图形计算。decimal类型通常用于金融计算这种十进制下的高精度算术运算。

2.4.1 数值字面量

整数类型字面量可以使用十进制或者十六进制表示。十六进制辅以0x前缀。例如:

int x = 127;
long y = 0x7F;

从C# 7开始,可以在数值字面量的任意位置加入下划线以方便阅读:

int million = 1_000_000;

C# 7还可以用0b前缀使用二进制表示数值:

var b = 0b1010_1011_1100_1101_1110_1111;

实数字面量可以用小数或指数表示,例如:

double d = 1.5;
double million = 1E06;

2.4.1.1 数值字面量类型接口

默认情况下,编译器将数值字面量推断为double类型或是整数类型。

  • 如果这个字面量包含小数点或者指数符号(E),那么它是double。
  • 否则,这个字面量的类型就是下列能满足这个字面量的第一个类型:int、uint、long和ulong。

例如:

Console.WriteLine (        1.0.GetType());  // Double  (double)
Console.WriteLine (       1E06.GetType());  // Double  (double)
Console.WriteLine (          1.GetType());  // Int32   (int)
Console.WriteLine ( 0xF0000000.GetType());  // UInt32  (uint)
Console.WriteLine (0x100000000.GetType());  // Int64   (long)

2.4.1.2 数值后缀

数值后缀显式定义了字面量的类型。后缀可以是下列小写或大写字母:

image.png

一般U和L后缀是很少需要的。因为uint、long和ulong总是可以推断出来或者从int类型隐式转换过来:

long i = 5;     // Implicit lossless conversion from int literal to long

从技术上讲,后缀D是多余的。因为所有带小数点的字面量都会推定为double类型。因此可以直接在数值字面量后加上小数点:

double x = 4.0;

后缀F和M是最有用的,并应该在指定float或decimal字面量时使用。下面的语句不能在没有后缀F时进行编译。这是因为4.5会认定为double而double是无法隐式转换为float的:

float f = 4.5F;

同样的规则也适用于decimal字面量:

decimal d = -1.23M;     // Will not compile without the M suffix.

我们将在下一节详细介绍数值转换的语义。

2.4.2 数值转换

2.4.2.1 整数类型到整数类型的转换

整数类型转换在目标类型能够表示源类型的所有可能值时是隐式转换,否则需要显式转换。例如:

int x = 12345;       // int is a 32-bit integer
long y = x;          // Implicit conversion to 64-bit integral type
short z = (short)x;  // Explicit conversion to 16-bit integral type

2.4.2.2 浮点类型到浮点类型的转换

double能表示所有可能的float值,因此float能隐式转换为double。反之则必须是显式转换。

2.4.2.3 浮点类型到整数类型的转换

所有整数类型可以隐式转换为浮点数类型:

int i = 1;
float f = i;

反之则必须是显式转换:

int i2 = (int)f;

将浮点数转换为整数时,小数点后的数值将被截去而不会舍入。静态类System.Convert提供了在不同值类型之间转换的舍入方法(见第6章)。
将大的整数类型隐式转换为浮点类型会保留数值部分,但是有时会丢失精度。这是因为浮点类型虽然拥有比整数类型更大的数值,但是有时其精度却比整数类型要小。以下代码用一个更大的数重复上述示例展示了这种精度丢失的情况:

int i1 = 100000001;
float f = i1;          // Magnitude preserved, precision lost
int i2 = (int)f;       // 100000000

2.4.2.4 decimal类型转换

所有的整数类型都能隐式转换为decimal类型。这是因为decimal可以表示所有可能的C#整数类型值。其他所有的数值类型转换为decimal或从decimal类型进行转换都必须是显式转换。

2.4.3 算术运算符

算式运算符(+、-、*、/、%)可用于除8位和16位的整数类型之外的所有数值类型:

+    Addition
-    Subtraction
*    Multiplication
/    Division
%    Remainder after division

2.4.4 自增和自减运算符

自增和自减运算符(++、--)分别给数值类型加1或者减1。具体要将其放在变量之前还是之后则取决于需要得到变量在自增/自减之前的值还是之后的值。例如:

int x = 0, y = 0;
Console.WriteLine (x++);   // Outputs 0; x is now 1
Console.WriteLine (++y);   // Outputs 1; y is now 1

2.4.5 特殊整数类型运算

(整数类型指int、uint、long、ulong、short、ushort、byte和sbyte。)

2.4.5.1 整数除法

整数类型的除法运算总是会截断余数(向0舍入)。用一个值为0的变量做除数将产生运行时错误(DivideByZeroException):

int a = 2 / 3;      // 0

int b = 0;
int c = 5 / b;      // throws DivideByZeroException

用字面量式常量0做除数将产生编译时错误。

2.4.5.2 整数溢出

在运行时执行整数类型的算术运算可能会造成溢出。默认情况下,溢出会默默地发生而不会抛出任何异常,且其溢出行为是“循环”的。就像是运算发生在更大的整数类型上,而超出部分的进位就被丢弃了。例如,减少最小的整数值将产生最大的整数值:

int a = int.MinValue;
a--;
Console.WriteLine (a == int.MaxValue); // True

2.4.5.3 整数运算溢出检查运算符

checked运算符的作用是:在运行时当整数类型表达式或语句超过相应类型的算术限制时不再默默地溢出,而是抛出OverflowException。checked运算符可在有++、--、+、-(一元运算符和二元运算符)、*、/和整数类型间显式转换运算符的表达式中起作用。
checked运算符对double和float类型没有作用(它们会溢出为特殊的“无限”值,这会在后面介绍),对decimal类型也没有作用(这种类型总是会进行溢出检查)。
checked运算符能和表达式或语句块结合使用,例如:

int a = 1000000;
int b = 1000000;

int c = checked (a * b);      // Checks just the expression.

checked                       // Checks all expressions
{                             // in statement block.
   ...
   c = a * b;
   ...
}

可以在编译时加上/checked+命令行开关(在Visual Studio中,可以在“Advanced Build Settings”中设置)来默认使程序中所有表达式都进行算术溢出检查。如果你只想禁用指定表达式或语句的溢出检查,可以用unchecked运算符。例如,下面的代码即使在编译时使用了/checked+也不会抛出异常:

int x = int.MaxValue;
int y = unchecked (x + 1);
unchecked { int z = x + 1; }

2.4.5.4 常量表达式的溢出检查

无论是否使用了/checked编译器开关,编译时的表达式计算总会检查溢出,除非应用了unchecked运算符。

int x = int.MaxValue + 1;               // Compile-time error
int y = unchecked (int.MaxValue + 1);   // No errors

2.4.5.5 位运算符

C#支持以下的位运算符:

image.png

2.4.6 8位和16位整数类型

8位和16位整数类型指byte、sbyte、short、ushort。这些类型自己并不具备算术运算符,所以C#隐式地将它们转换为所需的更大一些的类型。当试图把运算结果赋给一个小的整数类型时会产生编译时错误:

short x = 1, y = 1;
short z = x + y;          // Compile-time error

在以上情况下,x和y会隐式转换成int以便进行加法运算。因此运算结果也是int,它不能隐式转换回short(因为这可能会造成数据丢失)。我们必须使用显式转换才能令其通过编译:

short z = (short) (x + y);   // OK

2.4.7 特殊的float和double值

不同于整数类型,浮点类型包含某些特定运算需要特殊对待的值。这些特殊的值是NaN(Not a Number,非数字)、+∞、-∞和-0。float和double类型包含表示NaN、+∞、-∞值的常量。其他的常量还有MaxValue、MinValue以及Epsilon。例如:

Console.WriteLine (double.NegativeInfinity);   // -Infinity

double和float类型的特殊值的常量表如下:

image.png

非零值除以零的结果是无穷大。例如:

Console.WriteLine ( 1.0 / 0.0);                  //  Infinity
Console.WriteLine (-1.0 / 0.0);                  // -Infinity
Console.WriteLine ( 1.0 / -0.0);                  // -Infinity
Console.WriteLine (-1.0 / -0.0);                  //  Infinity

零除以零或无穷大减去无穷大的结果是NaN。例如:

Console.WriteLine ( 0.0 /  0.0);                 //  NaN
Console.WriteLine ((1.0 /  0.0) - (1.0 / 0.0));   //  NaN

使用比较运算符(==)时,一个NaN的值永远也不等于其他的值,甚至不等于其他的NaN值:

Console.WriteLine (0.0 / 0.0 == double.NaN);    // False

必须使用float.IsNaN或double.IsNaN方法来判断一个值是否为NaN:

Console.WriteLine (double.IsNaN (0.0 / 0.0));   // True

但使用object.Equals方法时,两个NaN却是相等的:

Console.WriteLine (object.Equals (0.0 / 0.0, double.NaN));   // True

NaN在表示特殊值时很有用。在WPF中,double.NaN表示值为“Automatic”(自动)。另一种表示方法是使用可空类型(nullable,见第4章)。还可以使用一个包含数值类型和一个额外字段的自定义结构体(见第3章)。
float和double遵循IEEE 754格式类型规范。几乎所有的处理器都原生支持此规范。如需此类型行为的详细信息,可参考http://www.ieee.org

2.4.8 double和decimal的对比

double类型在科学计算(例如计算空间坐标)时很有用。decimal类型在金融计算和计算那些“人为”的而非真实世界度量的结果时很有用。下面是这两种类型的不同之处:

image.png

2.4.9 实数的舍入误差

float和double在内部都是基于2来表示数值的。因此只有基于2表示的数值才能够精确表示。事实上,这意味着大多数有小数部分的字面量(它们都基于10)将无法精确表示。例如:

float tenth = 0.1f;                       // Not quite 0.1
float one   = 1f;
Console.WriteLine (one - tenth * 10f);    // -1.490116E-08

这就是为什么float和double不适合金融运算。相反,decimal基于10,它能够精确表示基于10的数值(也包括它的因数,基于2和基于5的数值)。因为实数的字面量都是基于10的,所以decimal能够精确表示像0.1这样的数。然而,double和decimal都不能精确表示那些基于10的循环小数:

decimal m = 1M / 6M;               // 0.1666666666666666666666666667M
double  d = 1.0 / 6.0;             // 0.16666666666666666

这将会导致积累性的舍入误差:

decimal notQuiteWholeM = m+m+m+m+m+m;  // 1.0000000000000000000000000002M
double  notQuiteWholeD = d+d+d+d+d+d;  // 0.99999999999999989

这也将影响相等和比较操作:

Console.WriteLine (notQuiteWholeM == 1M);   // False
Console.WriteLine (notQuiteWholeD < 1.0);   // True

2.5 布尔类型和运算符

C#中的bool(System.Boolean类型的别名)类型是能赋值为true和false字面量的逻辑值。
尽管布尔类型的值仅需要1位的存储空间,但是运行时却使用了1字节内存空间。这是因为字节是运行时和处理器能够有效使用的最小单位。为避免在使用数组时的空间浪费,.NET Framework在System.Collections命令空间下提供了BitArray类,其中的每一个布尔值仅占用一位。

2.5.1 布尔类型转换

bool类型不能转换为数值类型,反之亦然。

2.5.2 相等和比较运算符

==和!=用于判断任意类型的相等与不等,并总是返回一个bool值。注3值类型通常有很简单的相等定义:

int x = 1;
int y = 2;
int z = 1;
Console.WriteLine (x == y);         // False
Console.WriteLine (x == z);         // True

对于引用类型,默认情况下相等是基于引用的,而不是底层对象的实际值(更多内容请参见第6章):

public class Dude
{
  public string Name;
  public Dude (string n) { Name = n; }
}
...
Dude d1 = new Dude ("John");
Dude d2 = new Dude ("John");
Console.WriteLine (d1 == d2);       // False
Dude d3 = d1;
Console.WriteLine (d1 == d3);       // True

相等和比较运算符==、!=、<、>、>=和<=可用于所有的数值类型,但是用于实数时要特别注意(请参见2.4.9节)。比较运算符也可以用于枚举(enum)类型的成员,它比较的是表示枚举成员的整数值,我们将在3.7节中介绍。
我们将在4.14节、6.11节和6.12节中详细介绍相等和比较运算符。

2.5.3 条件运算符

&&和||运算符用于判断与和或条件。它们常常与代表“非”的!运算符一起使用。在下面的例子中,UseUmbrella方法在下雨或阳光充足(雨伞可以保护我们不会经受日晒雨淋),以及无风(因为雨伞在有风的时候不起作用)的时候返回true:

static bool UseUmbrella (bool rainy, bool sunny, bool windy)
{
  return !windy && (rainy || sunny);
}

&&和||运算符会在可能的情况下执行短路计算。在上面的例子中。如果刮风,(rainy || sunny)将不会计算。短路计算在某些表达式中是非常必要的,它可以允许如下表达式运行而不会抛出NullReferenceException异常:

if (sb != null && sb.Length > 0) ...

&和|运算符也可用于判断与和或条件:

return !windy & (rainy | sunny);

不同之处是&和|运算符不支持短路计算。因此它们很少用于替代条件运算符。
不同于C和C++,&和|运算符在用于布尔表达式时执行布尔比较(非短路计算)。而&和|运算符仅在用于数值运算时才执行位运算。
(三元)条件运算符
三元条件运算符(由于它是唯一一个使用三个操作数的运算符,因此也简称为三元运算符)使用q ? a : b的形式。它在q为真时计算a否则计算b。例如:

static int Max (int a, int b)
{
  return (a > b) ? a : b;
}

条件运算符在LINQ语句中尤其有用(见第8章)。

2.6 字符串和字符

C#的char(System.Char类型的别名)类型表示一个Unicode字符并占用两个字节。char字面量应位于两个单引号之间:
char c = 'A'; // Simple character
转义字符指那些不能用字面量表示或解释的字符。转义字符由反斜线和一个表示特殊含义的字符组成,例如:
char newLine = 'n';
char backSlash = '\';
表2-2中列出了转义字符序列。

image.png
image.png

u(或x)转义字符通过4位十六进制代码来指定任意Unicode字符:

char copyrightSymbol = '\u00A9';
char omegaSymbol     = '\u03A9';
char newLine         = '\u000A';

2.6.1 char转换

从char类型到数值类型的隐式转换只在这个数值类型可以容纳无符号short类型时有效。对于其他的数值类型,则需要显式转换。

2.6.2 字符串类型

C#中的字符串类型(System.String类型的别名,我们将在第6章详细介绍)表示不可变的Unicode字符序列。字符串字面量应位于两个双引号(")之间:


						

上一篇: 简化Linq to SQL查询:子查询、in操作和join详解

下一篇: 全面解析Linq中的GroupBy方法应用