第1章 C#概述

第1章 C#概述

C#是一种成熟的语言,它基于C风格语言(C、C++和Java)的特性而设计,有经验的程序员很快就能熟悉它。[1]作为构建软件组件和应用程序的编程语言,C#是更大、更复杂的开源执行平台——公共语言基础结构(Common Language Infrastructure,CLI)——的一部分。

本章使用传统HelloWorld程序介绍C#,重点是C#语法基础,包括在C#程序中定义入口。通过本章的学习,将熟悉C#的语法风格和结构,并能写最简单的C#程序。讨论C#语法基础之前,将简单介绍托管执行环境,并解释C#程序在运行时如何执行。本章最后会讨论如何声明变量、如何在控制台上写入和获取数据以及如何为C#代码添加注释。

图像说明文字

1.1 Hello, World

学习新语言最好的办法就是动手写代码。第一个例子是经典HelloWorld程序。这个程序在屏幕上显示一些文本。

代码清单1- 1展示了完整的HelloWorld程序,在之后的小节中还将介绍代码的编译方法。

代码清单1-1 用C#编写的HelloWorld[2]

classHelloWorld
{
static void Main()
 {
   System.Console.WriteLine("Hello. My name is Inigo Montoya.");
 }
}

注意

 

C#是区分大小写的语言;大小写不正确,会使代码无法成功编译。

有Java、C或者C++编程经验的读者很快就能看出相似的地方。类似于Java,C#也从C和C++继承了基本的语法。②3语法标点(如分号和大括号)、特性(如区分大小写)和关键字(如classpublicvoid)对于这些程序员来说并不陌生。初学者和有其他语言背景的程序员通过这个程序能很快体会到这些构造的直观性。

编译并运行应用程序

C#编译器允许为C#源代码文件使用任何文件扩展名,但一般使用.cs。将源代码保存到文件后,开发者必须编译它(附录A会指导你安装编译器)。由于C#标准没有规定命令机制,所以实际的编译命令取决于C#编译器的实现。

假定将代码清单1-1的代码放到一个名为HelloWorld.cs的文件中,而且使用的是Microsoft.NET编译器,编译器的路径也已设置好,就可以执行如输出1-1所示的编译命令。 [4]

输出1-1

>csc.exe HelloWorld.cs
Microsoft (R) Visual C# Compiler version 1.0.0.50618
Copyright (C) Microsoft Corporation. All rights reserved.

具体输出将取决于所用的编译器的版本。

运行生成的程序HelloWorld.exe,将显示如输出1-2所示的消息。

输出1-2

>HelloWorld.exe
Hello. My name is Inigo Montoya.

C#编译器创建的程序HelloWorld.exe是一个程序集(assembly)。虽然在这个例子中创建的是一个能独立运行的程序,但也可以不这么做,而是创建能由另一个较大的程序引用的代码库。库(或称为类库)的文件扩展名是.dll,其中dll代表“动态链接库”(Dynamic Link Library,DLL)。库也是一个程序集。换言之,一次成功的C#编译生成的肯定是程序集,无论它是程序还是库。

语言对比:Java——文件名必须和类名匹配

 

在Java中,文件名必须和类名一致。C#虽然也常常遵守这一约定,但却不是必需的。在C#中,一个文件可以包含多个类;而且从C# 2.0开始,一个类的代码可以拆分到多个文件中,这一特性称为部分类。

1.2 C#语法基础

成功编译并运行HelloWorld程序之后,我们来分析代码,了解它的各个组成部分。首先熟悉一下C#关键字以及开发者选用的标识符。

初学者主题:关键字

 

为了帮助编译器解释代码,C#中的某些单词具有特殊地位和含义,我们将其称为关键字。关键字提供了具体的语法,编译器根据这些语法来解释程序员书写的表达式。在HelloWorld程序中,classstaticvoid均是关键字。

编译器利用关键字来识别代码的结构与组织方式。因为编译器对这些单词有着严格的解释,所以开发人员只能按照C#的语言规则将关键字放在特定的位置。一旦程序员违反规则,编译器就会报错。

1.2.1 C#关键字

表1-1总结了C#关键字。

表1-1 C#关键字

abstract

enum

long

static

add* (1)

equals* (3)

nameof* (6)

string

alias* (2)

event

namespace

struct

as

explicit

new

switch

ascending* (3)

extern

null

this

async* (5)

false

object

throw

await* (5)

finally

on* (3)

true

base

fixed

operator

try

bool

float

orderby* (3)

typeof

break

for

out

uint

by* (3)

foreach

override

ulong

byte

from* (3)

params

unchecked

case

get* (1)

partial* (2)

unsafe

catch

global* (2)

private

ushort

char

goto

protected

using

checked

group* (3)

public

value* (1)

class

if

readonly

var* (3)

const

implicit

ref

virtual

continue

in

remove*(1)

void

decimal

int

return

volatile

default

interface

sbyte

where* (2)

delegate

internal

sealed

when* (6)

descending* (3)

into* (3)

select* (3)

while

do

is

set* (1)

yield* (2)

double

join* (3)

short

 

dynamic* (4)

let* (3)

sizeof

 

else

lock

stackalloc

 

  • 表示上下文关键字,括号中的数字代表加入该上下文关键字的C#版本。

C# 1.0之后没有引入任何新的保留关键字,但在后续版本中,一些构造使用了上下文关键字(contextual keyword),它们在特定位置才有意义。除了那些位置,上下文关键字没有任何特殊意义。[5]这样,大多数的C# 1.0代码都完全兼容于后续的版本。[6]

1.2.2 标识符

和其他语言一样,C#用标识符标识程序员编写代码的构造。在代码清单1-1中,HelloWorldMain都是标识符。分配标识符之后,以后就能用它引用所标识的构造。因此,开发人员应分配有意义的名称,不要随意分配。

好的程序员总能选择简洁而有意义的名称,这使代码更容易理解和重用。清晰和一致非常重要,.NET Framework Guidelines(http://bit.ly/dotnetguidelines)建议不要在标识符中使用单词缩写[7],甚至不要使用不被广泛接受的首字母缩写词。即使缩写被广泛接受(如HTML),使用时也要一致,不要忽而这样用,忽而那样用。为避免滥用,可限制所有首字母缩写词都必须包含到术语表中。总之,要选择清晰(甚至是详细)的名称,尤其是在团队中工作,或者开发别人要使用的库的时候。

标识符有两种基本的大小写风格。第一种风格是CLI创建者所谓的Pascal大小写(PascalCase),它在Pascal编程语言中很流行,要求标识符中每个单词的首字母大写,例如ComponentModelConfigurationHttpFileCollection。注意在HttpFileCollection中,由于首字母缩写词HTTP的长度超过两个字母,所以仅首字母大写。第二种风格是驼峰大小写(camelCase),即除了第一个字母小写,其他约定与Pascal大小写风格一样,例如quotientfirstNamehttpFileCollectionioStreamtheDreadPirateRoberts

规范

 

要更注重标识符的清晰而不是简短。

不要在标识符名称中使用单词缩写。

不要使用不被广泛接受的首字母缩写词,即使被广泛接受,非必要时也不要用。

下划线虽然合法,但标识符中一般不要包含下划线、连字符或其他非字母/数字字符。此外,C#不像其前辈那样使用匈牙利命名法(为名称附加类型缩写前缀)。这避免了数据类型改变时还要重命名变量,也避免了因调整数据类型前缀失效引起的不一致的情况。

在极少数情况下,有的标识符(如Main)可能在C#语言中具有特殊含义。

规范

 

要把只包含两个字母的首字母缩写词全部大写,除非它是驼峰大小写风格标识符的第一个单词。

包含3个或更多字母的首字母缩写词,仅第一个字母才要大写,除非该缩写词是驼峰大小写风格标识符的第一个单词。

在驼峰大小写风格标识符开头的首字母缩写词中,所有字母都不要大写。

不要使用匈牙利命名法(也就是,不要为变量名称附加类型前缀)。

 

高级主题:关键字

 

虽然比较罕见,但关键字附加“@”前缀可作为标识符使用。例如,可命名局部变量@return。类似地(虽不符合C#大小写规范),可命名方法@throw()

在微软的实现中,还有4个未文档化的保留关键字,即__arglist__makeref__reftype__refvalue。它们仅在罕见的互操作情形下才会用到,平时完全可以忽略。需要注意的是,这4个特殊关键字以双下划线开头。C#设计者保留将来把这种标识符转化为关键字的权利。为安全起见,开发人员自己不要创建这样的标识符。

1.2.3 类型定义

C#中所有代码都出现在一个类型定义的内部,最常见的类型定义是以关键字class开头的。如代码清单1-2所示,类定义(class definition)是“class标识符{ ... }”形式的一个代码块。

代码清单1-2 基本的类声明

class HelloWorld
{
//...
}

类型的名称(本例是HelloWorld)可以随便取,但根据约定,它应当使用Pascal大小写风格。就本例来说,可以选择的名称包括GreetingsHelloInigoMontoyaHello,或者简单地称为Program。(对于包含Main()方法的类,Program是个很好的名称。Main()方法将在稍后详述。)

规范

 

要用名词或名词短语命名类。

要为所有类名使用Pascal大小写风格。

一个程序通常包含多个类型,每个类型都包含多个方法。

1.2.4 Main

初学者主题:什么是方法?

 

从语法上说,C#程序中的方法是一个已命名的代码块,该代码块由一个方法声明(如static void Main())引入,后跟一对大括号({}),其中包含零或多条语句。方法可以执行计算或者行动。与书写语言中的段落相似,方法提供了结构化和组织代码的一种方式,使之更易读。更重要的是,方法可以重用,可以在多个地方调用,所以避免了代码的重复。方法声明除了负责引入方法之外,还要定义方法名以及要传入和传出方法的数据。在代码清单1-3中,Main()后跟{ ... }便是一个C#方法的例子。

C#程序从Main方法开始执行。该方法以static void Main()开头。在命令控制台中输入HelloWorld.exe执行程序,程序会启动并解析Main的位置,然后执行其中第一条语句,如代码清单1-3所示。

代码清单1-3 HelloWorld分解示意图

图像说明文字

虽然Main方法声明可以进行某种程度的改变,但关键字static和方法名Main是始终都是程序必需的。

高级主题:Main方法的声明

 

C#要求Main方法的返回类型为voidint,而且要么不带参数,要么接收一个字符串数组作为参数。代码清单1-4展示了Main方法的完整声明。

代码清单1-4 带有参数和返回类型的Main方法

staticintMain(string[] args)
{
//...
    }

args参数是一个字符串数组,用于接收命令行参数。但此数组的第一个元素不是程序名称,而是可执行文件名称之后的第一个命令行参数,这一点与C和C++不同。要获取执行程序所用的完整命令,可以使用System.Environment.CommandLine

Main返回的int值是状态码,标识程序执行是否成功。返回非零值通常意味着错误。

语言对比:C++/Java——main()是全部小写的

 

与C风格的“前辈们”不同,C#的Main方法名使用大写M,以便与C#的Pascal大小写风格命名约定保持一致。

Main方法指定为static意味着这是“静态”方法,可用“类名.方法名”的形式调用它。如果不指定static,用于启动程序的命令控制台还要先对类进行实例化(instantiation),然后才能调用方法。第5章会用一节的篇幅专门讲述静态成员。

Main()之前的void表明该方法不返回任何数据(第2章会进一步解释)。

C#和C/C++一样使用大括号封闭构造(如类或者方法)的主体。例如,Main方法的主体就是用大括号封闭起来的。在本例中,方法的主体只有一条语句。

1.2.5 语句和语句分隔符

Main方法只包含一条语句,即System.Console.WriteLine();,它在控制台上输出一行文本。C#通常用分号标识语句结束,每条语句都由代码要执行的一个或多个行动构成。声明变量、控制程序流程或者调用方法,所有这些都是语句的例子。

语言对比:Visual Basic——基于行的语句

 

有的语言以行为基本单位,这意味着不加上特殊标记,语句便不能跨行。在Visual Basic 2010以前,Visual Basic一直是典型的基于行的语言。它要求在行末添加下划线表示语句跨越多行。从Visual Basic 2010开始,行连续符在许多时候都变成可选的。

 

高级主题:没有分号的语句

 

C#的许多编程元素都以分号结尾。不要求使用分号的例子是switch语句。由于大括号总是包含在switch语句中,所以C#不要求语句后加上分号。事实上,代码块本身就被视为语句(它们也由语句构成),不要求以分号结尾。类似地,有的编程元素(如using指令)虽然末尾有分号但不被视为语句。

由于换行与否不影响语句的分隔,所以可以将多条语句放到同一行,C#编译器会认为这一行包含多条指令。例如,代码清单1-5在同一行包含了两条语句。执行时,它们会在控制台窗口中分两行显示UpDown

代码清单1-5 一行中包含多条语句

System.Console.WriteLine("Up");System.Console.WriteLine("Down");

C#还允许一条语句跨越多行。同样地,C#编译器会根据分号判断语句的结束位置。代码清单1-6展示了一个例子。

代码清单1-6 一条语句跨越多行

System.Console.WriteLine(
"Hello. My name is Inigo Montoya.");

代码清单1-6的WriteLine()语句的原始版本来自HelloWorld程序,它在这里跨越了多行。

1.2.6 空白

分号使C#编译器能忽略代码中的空白。除了少数例外情况,C#允许在代码中随意插入空白而不改变其语义。在代码清单1-5和代码清单1-6中,在语句中或语句间换行都可以,对编译器最终创建的可执行文件没有任何影响。

初学者主题:什么是空白?

 

空白是一个或多个连续的格式字符(如制表符、空格和换行符)。删除单词间的所有空白肯定会造成歧义。删除引号字符串中的任何空白也会造成歧义。

程序员经常利用空白对代码进行缩进来增强可读性。来看看代码清单1-7和代码清单1-8展示的两个版本的HelloWorld程序。

代码清单1-7 不缩进

classHelloWorld
{
staticvoidMain()
{
System.Console.WriteLine("Hello Inigo Montoya");
}
}

代码清单1-8 删除一切可以删除的空白

class HelloWorld{staticvoid Main()

{System.Console.WriteLine("Hello Inigo Montoya");}}

虽然这两个版本看起来和原始版本颇有不同,但C#编译器认为这几个版本的代码没有任何区别。

初学者主题:用空白来格式化代码

 

为了增强可读性,利用空白对代码进行缩进是非常重要的。写代码时要遵循已经建立的编码标准和约定,以增强代码的可读性。

本书约定每个大括号都单独占一行,并缩进大括号之间的代码。假如一对大括号之间有第二对大括号,那么第二对大括号中的所有代码也要缩进。

这不是统一的C#标准,只是一种风格偏好。

1.2.7 使用变量

前面已接触了最基本的C#程序,下面让我们来声明一个局部变量。变量声明后就可以被赋值,将值替换成新值,并可在计算和输出等操作中使用。然而,变量一经声明,数据类型就无法改变。在代码清单1-9中,string max就是一个变量声明。

代码清单1-9 变量的声明和赋值

图像说明文字

初学者主题:局部变量

 

变量是一个符号名称,指向一个随时间变化的值。局部意味着是在方法内部声明变量。

声明变量就是定义它,需要:

(1)指定变量要包含的数据的类型;

(2)为它分配标识符(变量名)。

1.2.8 数据类型

代码清单1-9声明了 string类型的变量。本章还使用了 intchar数据类型。

  • int是指C#的32位整型。

  • char是字符类型,长度16位,足以表示无代理项的Unicode字符[8]

下一章将更详细地探讨这些以及其他常见数据类型。

初学者主题:什么是数据类型?

 

一个变量声明所指定的数据的类型称为数据类型。数据类型,或者简称为类型,是具有相似特征和行为的个体的分类。例如,animal(动物)就是一个类型,它对具有动物特征(多细胞、具有运动能力等)的所有个体(猴子、野猪和鸭嘴兽等)进行了分类。类似地,在编程语言中,类型是被赋予了相似特性的一些个体的定义。

1.2.9 变量的声明

在代码清单1-9中, string max是一个变量声明,它声明了一个名为 maxstring类型的变量。还可以在同一条语句中声明多个变量,办法是指定数据类型一次,然后用逗号分隔每个标识符,如代码清单1-10所示。

代码清单1-10 在一条语句中声明两个变量

string message1, message2;

由于声明多个变量的语句只允许开发者提供一次数据类型,因此所有变量都具有相同类型。

在C#中,变量名可以用任何字母或者下划线(_)开头,后跟任意数量的字母、数字或下划线。但根据约定,局部变量名采用的是驼峰大小写风格命名(即除了第一个单词,其他每个单词的首字母大写),而且不包含下划线。

规范

 

要为局部变量使用camel大小写风格的命名。

1.2.10 变量的赋值

局部变量声明后必须在引用之前为其赋值。一个办法是使用 =操作符,或者称为简单赋值操作符。操作符是一种特殊符号,标识了代码要执行的操作。代码清单1-11演示了如何利用赋值操作符指定变量 miracleMaxvalerie要指向的字符串值。

代码清单1-11 更改变量的值

class **StormingTheCastle
{
staticvoidMain()
  {
string valerie;

string miracleMax = "Have fun storming the castle!";

   valerie = "Think it will work?";

      System.Console.WriteLine(miralcleMax);
      System.Console.WriteLine(valerie);


   miracleMax = "It would take a miracle.";


      System.Console.WriteLin(miracleMax);
  }
}

从这个代码清单可以看出,既可以在声明变量的同时对它赋值(如变量 miracleMax),也可以在声明了变量之后用另一条语句赋值(如变量 valerie)。要赋的值必须放在赋值操作符右侧。

运行编译好的 StormingTheCastle.exe程序,会生成如输出1-3所示的结果。

输出1-3

>StormingTheCastle.exe
Have fun storming the castle!
Think it will work?
It would take a miracle.

C#要求局部变量在读取之前“明确赋值”。此外,一次赋值会返回一个值。所以,C#允许在同一条语句中进行多个赋值操作,如代码清单1-12所示。

代码清单1-12 赋值会返回一个值,该值可用于再次赋值

class StormingTheCastle
{
staticvoidMain()
  {
// ...
string requirements, miracleMax;
    requirements = miracleMax = "It would take a miracle.";
// ...
  }
}

1.2.11 变量的使用

赋值后就能用变量标识符引用值。因此,在 System.Console.WriteLine(miracleMax)语句中使用变量 miracleMax时,程序在控制台上显示 Have fun storming the castle!,也就是 miracleMax的值。更改 miracleMax的值并执行相同的 System.Console.WriteLine(miracleMax)语句,会显示 miracleMax的新值,即 It would take a miracle.

高级主题:字符串不可变

 

所有 string类型的数据,不管是不是字符串字面量(literal)[9],都是不可变的(或者说是不可修改的)。例如,不可能将字符串" Come As You Are"更改为" Come As You Age"。也就是说,不能修改变量最初引用的数据,只能重新为变量赋值,让它引用内存中的新位置。

1.3 控制台输入和输出

本章已多次使用 System.Console.WriteLine将文本输出到命令控制台。除了能输出数据,程序还需要能接收用户输入的数据。

1.3.1 从控制台获取输入

获取用户在控制台输入文本的一种方法是使用 System.Console.ReadLine()。此方法将暂停程序执行以便用户输入字符。一旦用户按回车键创建新的一行,程序就会继续执行。 System.Console.ReadLine()方法的输出,也称为返回值,就是用户输入的文本字符串。下面来看一下代码清单1-13以及输出1-4。

代码清单1-13 使用System.Console.ReadLine()

class HeyYou

{
staticvoid Main()
  {
string firstName;
string lastName;

      System.Console.WriteLine("Hey you!");

      System.Console.Write("Enter your first name: ");
      firstName = System.Console.ReadLine();

      System.Console.Write("Enter your last name: ");
      lastName = System.Console.ReadLine();
  }
}
 

输出1-4

>HeyYou.exe
Hey you!
Enter your first name: Inigo
Enter your last name: Montoya

在每条提示信息之后,程序都用 System.Console.ReadLine()方法获取用户输入的文本,并将其赋给恰当的变量。在第二个 System.Console.ReadLine()赋值操作完成之后, firstName引用值 Inigo,而 lastName引用值 Montoya

高级主题: System.Console.Read()

 

除了 System.Console.ReadLine()之外,还有 System.Console.Read()方法。但 System.Console.Read()方法返回的是与读取的字符值对应的整数,如果没有更多的字符可用,就返回-1。为了获取实际字符,需要先将整数转型为字符,如代码清单1-14所示。

代码清单1-14 使用System.Console.Read()

int readValue;
charcharacter;
readValue = System.Console.Read();
character = (char) readValue;
System.Console.Write(character);

注意,除非用户按回车键,否则System.Console.Read()方法不会返回输入。按回车键之前不会对字符进行处理,即使用户已经输入了多个字符。

C# 2.0以上的版本可以使用 System.Console.ReadKey()方法。它和 System.Console.Read()方法不同,用户每按下一个键就返回用户所按的键。可用它拦截用户按键操作,并执行相应行动,如校验按键,限制只能按数字键。

1.3.2 将输出写入控制台

代码清单1-13中是用 System.Console.Write()而不是 System.Console.WriteLine()方法提示用户输入他的名和姓。 System.Console.Write()方法不在显示文本之后添加一个换行符,而是将当前位置保持在同一行上。这样用户输入的内容就会与提示内容处于同一行。代码清单1-13的输出清楚地演示了 System.Console.Write()的效果。

下一步是将使用 System.Console.ReadLine()获取的值写回控制台。在代码清单1-15所示的例子中,程序将在控制台上输出用户的全名。但这段代码使用的是 System.Console. WriteLine()的一个变体。输出1-5展示了对应的输出。

代码清单1-15 使用字符串插值进行格式化

class HeyYou
{
staticvoid Main()
  {
string firstName;
string lastName;

      System.Console.WriteLine("Hey you!");

      System.Console.Write("Enter your first name: ");
      firstName = System.Console.ReadLine();

      System.Console.Write("Enter your last name: ");
      lastName = System.Console.ReadLine();

   System.Console.WriteLine(

$"Your full name is { firstName } { lastName }.");

  }
}
 

输出1-5

Hey you!
Enter your first name: Inigo
Enter your last name: Montoya
Your full name is Inigo Montoya.

在代码清单1-15所示的例子中,不是先用 Write语句输出 Your full name is,再用 Write语句输出 firstName,然后用第三条 Write语句输出空格,最后用 WriteLine语句输出 lastName。这个例子利用了C# 6.0的字符串插值(string interpolation)来进行一次性输出。在字符串插值中,编译器将字符串花括号中的部分解释为可以嵌入代码(表达式)的区域,编译器将对嵌入的表达式估值并将其转换为字符串。字符串插值不需要先逐个执行很多个代码片段,最后再将结果组合成字符串,它可以一步完成这些输出。这使得代码更容易理解。

C# 6.0之前的版本利用的是复合格式化(composite formatting)来进行一次性输出。在复合格式化中,代码首先提供格式字符串(format string)来定义输出格式,参考代码清单1-16。

代码清单1-16 使用System.Console.WriteLine()的复合格式化进行格式化

class HeyYou
{
staticvoid Main()
  {
string firstName;
string lastName;

      System.Console.WriteLine("Hey you!");

      System.Console.Write("Enter your first name: ");
      firstName = System.Console.ReadLine();

      System.Console.Write("Enter your last name: ");
      lastName = System.Console.ReadLine();

   System.Console.WriteLine(

"Your full name is {0} {1}.", firstName, lastName);

  }
}

本例的格式字符串是" Your full name is {0} {1}."。它为要在字符串中插入的数据标识了两个索引占位符。每一个占位符对应着格式字符串之后顺序出现的参数。

注意,索引值是从零开始的。每个要插入的参数,或者称为格式项(format item),按照与索引值对应的顺序排列在格式字符串之后。在本例中,由于 firstName是紧接在格式字符串之后的第一个参数,所以它对应于索引值0。类似地, lastName对应于索引值1。

注意,占位符在格式字符串中不一定按顺序出现。例如,代码清单1-17中的代码交换了两个索引占位符的位置并添加了一个逗号,从而改变了姓名的显示方式(参见输出1-6)。

代码清单1-17 交换索引占位符和对应的变量

  System.Console.WriteLine("Your full name is {1}, {0}", 
  firstName, lastName);

输出1-6

Hey you!
Enter your first name: Inigo
Enter your last name: Montoya
Your full name is Montoya, Inigo

占位符除了能在格式字符串中按任意顺序出现之外,同一个占位符还能在一个格式字符串中多次使用。另外,也可以忽略占位符。然而,每个占位符都必须对应一个参数,不能使用没有对应参数的占位符。

因为C# 6.0风格的字符串插值比复合字符串方式更易理解,本书的后续章节我们将默认使用字符串插值方法。

1.3.3 注释

本节将修改代码清单1-16中的程序,在其中添加注释。注释不会改变程序的执行,只是使代码变得更容易理解。代码清单1-18中展示了新的代码,输出1-7展示了对应的输出。

代码清单1-18 在代码中添加注释

图像说明文字

输出1-7

Hey you!
Enter your first name: Inigo
Enter your last name: Montoya
Your full name is Inigo Montoya.

虽然插入了注释,但编译和执行新程序之后生成的输出和以前是一样的。

程序员使用注释来描述和解释他们写的代码,尤其是在语法本身难以理解的时候,或者是在另辟蹊径实现一个算法的时候。只有检查代码的程序员才需要看注释,编译器会忽略注释,因而生成的程序集中看不到源代码中的注释的一丝踪影。

表1-2总结了4种不同的C#注释。代码清单1-18使用了其中的两种。

表1-2 C#注释类型

注释类型

说 明

例 子

带分隔符的注释

一个正斜杠,后跟一个星号,即/*,用于开始一条带分隔符的注释。要结束注释,需要在星号之后跟上一个正斜杠,即*/。这种形式的注释可在代码文件中跨越多行,也可以在一行代码中嵌入使用。如果星号出现在行首,同时又在/*和*/这两个分隔符之间,那么它们也是注释的一部分,仅用来进行格式处理

/*注释*/

单行注释

注释也可以放在由两个连续的正斜杠构成的分隔符(//)之后。编译器将从这个分隔符开始到行末的所有文本视为注释。这种形式的注释只占一行。但是,可以连续使用多条单行注释,就像代码清单1-18最后的注释

//注释

XML带分隔符的注释

以/开头并以/结尾的注释称为XML带分隔符的注释。它们具有与普通的带分隔符的注释一样的特征,只是编译器会注意到XML注释的存在,而且可以把它们放到一个单独的文本文件中†

/**注释**/

XML单行注释

XML单行注释以///开头,并延续到行末。除此之外,编译器可以将XML单行注释和XML带分隔符的注释一起存储到单独的文件中

///注释

XML带分隔符的注释是C# 2.0中新增的,但它的语法完全与C# 1.0兼容。

第9章将更全面地讨论XML注释。届时将讨论XML标准的各种XML标记。

编程史上,确实有一段时期认为,如果一个程序员编写的代码没有详尽的注释,都不好意思说自己是专业和有经验的程序员。但是时代变了。没有注释但可读性好的代码比需要注释才能说清楚的代码更具价值。如果开发人员发现需要写注释才能说清楚代码块的功用,就应该考虑更清楚地重写代码,而不是洋洋洒洒写一堆注释。写注释其实就是重复代码思路的过程。不仅会造成混乱,降低可读性,还很容易过时,因为将来可能更改代码却没有及时更新注释。

规范

 

不要使用注释,除非代码本身“一言难尽”。

要尽量编写清晰的代码,而不是通过注释澄清复杂的算法。

 

初学者主题:可扩展标记语言

 

可扩展标记语言(Extensible Markup Language,XML)是一种简单而又灵活的文本格式,常用于Web应用程序以及应用程序间的数据交换。XML之所以是“可扩展”的,是因为XML文档中包含的是对数据进行描述的信息,也就是所谓的元数据(metadata)。下面是一个示例XML文件:

<?xml version="1.0" encoding="utf-8" ?>
<body>
  <book title="Essential C# 6.0">
      <chapters>
          <chapter title="Introducing C#"/>
          <chapter title="Operators and Control Flow"/>
          ...
      </chapters>
  </book>
</body>

文件以 Header元素开始,它描述了XML文件的版本和字符编码方式。之后是“book”元素。元素以尖括号中的单词开头,如< body>。为了结束元素,要将同一个单词放在尖括号中,同时为单词添加一个正斜杠前缀,如< /body>。除了元素,XML还支持属性。 title="Essential C# 6.0"就是XML属性的例子。注意,XML文件包含了对数据(如“Essential C# 6.0”、“Operators and Control Flow”等)进行描述的元数据(书名、章名等)。这可能形成相当臃肿的文件,但优点是提供了描述来帮助解释数据。

1.3.4 应用程序接口

一种数据类型(例如 System.Console)的所有方法(或者更一般地,也包括所有成员)定义了这种类型的应用程序接口(Application Programming Interface,API)。API定义了软件程序如何与部件交互。因此,不是一种数据类型,而是一组数据类型的所有API的结合创建了这组部件集合的API。例如,在.NET中,一个程序集包含的所有类型(以及这些类型的成员)构成这个程序集的API。同样,对于程序集的组合,例如.NET Framework中的程序集组合,每个程序集的API组合在一起构成一个更大的API。这个更大的API组通常被称为框架(framework),.NET Framework就是指.NET包含的所有程序集对外暴露的API。一般地,API包括一系列接口和协议(或指令),它们定义了程序和一组部件交互的规则。实际上,在.NET中,协议本身就是.NET程序集执行的规则。

1.3.5 托管执行和公共语言基础结构

处理器不能直接解释程序集。程序集采用的是另一种语言,即公共中间语言(Common Intermediate Language,CIL),或简称为中间语言(IL)[10]。C#编译器将C#源代码文件转换成这种中间语言。为了将CIL代码转换成处理器能理解的机器码,还要完成一个额外的步骤(通常在执行时进行)。该步骤涉及C#程序执行中的一个重要元素:虚拟执行系统(Virtual Execution System,VES)。VES偶尔也称为运行时,它根据需要编译CIL代码,这个过程称为即时编译或者JIT编译(just-in-time compilation)。假如代码在像“运行时”这样的一个“代理”的上下文中执行,就将这些代码称为托管代码(managed code),而在“运行时”的控制下执行的过程称为托管执行(managed execution)。之所以称为托管代码,是因为“运行时”管理着诸如内存分配、安全性和JIT编译等方面,从而控制了主要的程序行为。执行过程中不需要“运行时”的代码称为本机代码(native code)或非托管代码(unmanaged code)。

VES规范被包含在一个包容面更广的规范—CLI(Common Language Infrastructure,公共语言基础结构)规范[11]中,作为国际标准,CLI包含了以下几方面的规范。

  • VES或“运行时”。
  • CIL。
  • 为语言互操作性提供支持的类型系统,称为公共类型系统(Common Type System,CTS)。
  • 如何编写能通过CLI兼容语言访问的库的指导原则,这部分内容放在公共语言规范(Common Language Specification,CLS)中。
  • 使各种服务能被CLI识别的元数据(包括程序集的布局或文件格式规范)。
  • 一个公共编程框架,称为基类库(Base Class Library,BCL),所有语言的开发者都能利用它。

注意

 

“运行时”既可能指“程序执行的时候”,也可能指“虚拟执行系统”。为了明确起见,本书使用“执行时”来表示“程序执行的时候”,用“运行时”表示负责管理C#程序执行的代理。

如果是在一个CLI实现的上下文中运行,程序员不需要直接写代码就能使用大量服务和功能,包括如下几个。

  • 语言互操作性:不同源语言间的互操作性。语言编译器将每种源语言转换成相同中间语言(CIL)来实现这种互操作性。
  • 类型安全:检查类型间转换,确保只在兼容类型之间转换。这有助于防止发生缓冲区溢出——安全问题的一个主要诱因。
  • 代码访问安全性:程序集开发者的代码有权在计算机上执行的证明。
  • 垃圾回收:一种内存管理机制,自动释放“运行时”为数据分配的空间。
  • 平台可移植性:允许程序集在多种操作系统中运行。要做到这一点,一个显而易见的限制就是不能使用平台相关的库。所以,同Java一样,不可避免地有一些“个性”问题需要解决。
  • BCL:提供开发者能够(在所有CLI实现中)依赖的大型代码库,使他们不必亲自编写这些代码。

注意

 

本节简单介绍CLI,目的是熟悉C#程序的执行环境。本节还提及了本书后面将会用到的一些术语。第21章专门探讨CLI及其与C#开发者的关系。虽然那一章在本书的最后,但它的内容并不依赖于之前的任何章节。假如想多了解CLI,随时都可以跳到那一章。

1.3.6 C#和.NET版本

因为微软不同的产品团队的版本管理机制不同,所以.NET Framework和其对应的C#版本号并不一致。这意味着,假如你使用C# 6.0的编译器,默认编译的是.NET Framework 4.6。表1-3简单总结了C#和.NET的版本。

表1-3 C#和.NET版本

版  本

描  述

C# 1.0和.NET Framework 1.0/1.1 (Visual Studio 2002和2003)

C#的第一个正式发行版本。微软的团队从无到有创造了一种语言,专门为.NET编程提供支持

C# 2.0和.NET Framework 2.0 (Visual Studio 2005)

C#语言开始支持泛型,.NET Framework 2.0新增了支持泛型的库

.NET Framework 3.0

新增了一组API来支持分布式通信(Windows Communication Foundation,WCF)、富客户端表示(Windows Presentation Foundation,WPF)、工作流(Windows Workflow,WF)以及Web身份验证(Cardspaces)

C# 3.0和.NET Framework 3.5 (Visual Studio 2008)

添加了对LINQ的支持,对集合编程API进行了大幅改进。.NET Framework 3.5对原有的API进行了扩展以支持LINQ

C# 4.0和.NET Framework 4 (Visual Studio 2010)

添加了对动态类型的支持,对多线程编程API进行了大幅改进,强调了多处理器和核心支持

C# 5.0和.NET Framework 4.5 (Visual Studio 2012)和WinRT集成

添加了对异步方法调用的支持,同时不需要显式注册委托回调。在框架中新增了对Windows Runtime(WinRT)互操作性的支持

C# 6.0和.NET Framework 4.6 (Visual Studio 2015)

添加了字符串插值、null传播成员访问、异常过滤器、字典初始化以及大量其他的功能

只要编译器版本和代码要求的版本匹配,本书大多数代码都能在非微软平台上运行。虽然提供每种C#平台的完整细节能帮到一些读者,但也会使人分心,所以本书最终只包含和Microsoft .NET平台有关的细节。原因很简单,微软公司的实现到目前为止仍占统治地位。而且即使需要转移到其他平台,转移的过程也相当容易。

C# 6.0新增的最重要的框架功能是支持跨平台编译。换句话说,.NET Framework不仅可以运行在Windows操作系统上的,微软还提供了可以运行在Linux和OS X操作系统上的.NET Core的实现(CoreFX)。.NET Core不同于完整的.NET Framework功能集,它包含了整个(ASP.NET)网站可以在Windows之外的操作系统上部署所需的功能以及IIS(Internet Information Server,因特网信息服务器)。这意味着,同样的代码可以被编译和执行成跨平台运行的应用程序。.NET Core包含了.NET编译平台(“Roslyn”)、 .NET Core运行时、.NET版本管理(.NET Version Manager,DNVM)以及.NET执行环境(.NET Execution Environment,DNX)等工具,可以在Linux和OS X上执行。

1.3.7 CIL和ILDASM

前面说过,C#编译器将C#代码转换成CIL代码而不是机器码。处理器只能理解机器码,因此CIL代码必须先转换成机器码才能由处理器执行。给定一个程序集(DLL文件或可执行文件),可以使用CIL反汇编程序将其析构成对应的CIL表示,从而查看其CIL代码。通常使用微软特有的文件名 ILDASM来称呼这种CIL反汇编程序( ILDASM是IL Disassembler的简称),它能对程序或者它的类库执行反汇编,显示由C#编译器生成的CIL代码。 在不同的CLI实现中,使用CIL反汇编程序的命令也有所区别。可以像输出1-8展示的那样,在命令行中执行.NET CIL反汇编程序。

在不同的CLI实现中,使用CIL反汇编程序的命令也有所区别。可以像输出1-8展示的那样,在命令行中执行.NET CIL反汇编程序。

输出1-8

>ildasm /text HelloWorld.exe

使用/ text选项的目的是让输出在命令控制台上显示,而不是在新窗口中显示。执行上述命令得到的输出流是 HelloWorld.exe程序所含CIL代码的一个“转储”(dump)。CIL代码比机器码更容易理解。许多开发人员害怕即使别人没有拿到源代码,程序也容易被反汇编并曝光其算法。

无论是否基于CLI,任何程序防止反编译唯一安全的方法就是禁止访问编译好的程序(例如,只在网站上存放程序,而不把它分发到用户机器上)。但假如目的只是减小别人获得源代码的可能性,可以考虑使用一些混淆器(obfuscator)产品。这些混淆器会打开IL代码,将代码加密成一种功能不变但更难于理解的形式。这可以防止普通开发者访问代码,使程序集难以被反编译成容易理解的代码。除非程序需要对算法进行高级安全防护,否则混淆器足矣。

高级主题:HelloWorld.exeCIL输出

 

代码清单1-19展示了 ILDASM创建的CIL代码。

代码清单1-19 示例CIL输出

 //  Microsoft (R) .NET Framework IL Disassembler.  Version 4.6.81.0
 //  Copyright (c) Microsoft Corporation.  All rights reserved.



 // Metadata version: v4.0.30319
 .assembly extern mscorlib 
 {
   .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )                         //
➥.z\V.4..
   .ver 4:0:0:0 
 }
 .assembly HelloWorld
 {
   .custom instance void [mscorlib]System.Runtime.CompilerServices.
➥CompilationRelaxationsAttribute::.ctor(int32) = ( 01 00 08 00 00 00 00 00 ) 
   .custom instance void  [mscorlib]System.Runtime.CompilerServices.
➥RuntimeCompatibilityAttribute::.ctor() = ( 01 00 01 00 54 02 16 57 72 61 70
➥4E 6F 6E 45 78   // ....T..WrapNonEx
                                                                                                          ➥63 65 70 74 69 6F 6E 54 68 72 6F 77 73 01 )       // ceptionThrows.

   // --- The following custom attribute is added automatically, do not
➥uncomment -------
   // .custom instance void [mscorlib]System.Diagnostics.
➥DebuggableAttribute::.ctor(valuetype [mscorlib]System.Diagnostics.
➥DebuggableAttribute/DebuggingModes) = ( 01 00 07 01 00 00 00 00 )

  .hash algorithm 0x00008004
  .ver 0:0:0:0
 }
 .module HelloWorld.exe
 // MVID: {1FB5153C-639E-401D-8C94-22A66C18DC7A}
 .imagebase 0x00400000
 .file alignment 0x00000200
 .stackreserve 0x00100000
 .subsystem 0x0003       // WINDOWS_CUI
 .corflags 0x00000001    //  ILONLY
 // Image base: 0x01190000


 // =============== CLASS MEMBERS DECLARATION ==============

 .class public auto ansi beforefieldinit AddisonWesley.Michaelis.
 EssentialCSharp.Chapter01.Listing01_01.HelloWorld
        extends [mscorlib]System.Object
 {
   .method public hidebysig static void  Main() cil managed
   {
     .entrypoint
     // Code size       13 (0xd)
     .maxstack  8
     IL_0000:  nop
     IL_0001:  ldstr      "Hello. My name is Inigo Montoya."
     IL_0006:  call       void [mscorlib]System.Console::WriteLine(string)
     IL_000b:  nop
     IL_000c:  ret
   } // end of method HelloWorld::Main

   .method public hidebysig specialname rtspecialname 
           instance void  .ctor() cil managed
   {
     // Code size       8 (0x8)
     .maxstack  8
     IL_0000:  ldarg.0
     IL_0001:  call       instance void [mscorlib]System.Object::.ctor()
     IL_0006:  nop
     IL_0007:  ret
   } // end of method HelloWorld::.ctor

 } // end of class AddisonWesley.Michaelis.EssentialCSharp.Chapter01.
 Listing01_01.HelloWorld


 // =========================================================

 // *********** DISASSEMBLY COMPLETE ***********************

程序开头是清单(manifest)信息。其中不仅包括被反编译的模块的全名(HelloWorld.exe),还包括它依赖的所有模块和程序集及其版本信息。

基于这样的一个CIL代码清单,最有趣的可能就是能相对比较容易地理解程序所做的事情,这可比阅读并理解机器码(汇编程序)容易多了。上述代码中出现了对System.Console.WriteLine()的显式引用。CIL代码清单包含许多暂时没有什么用处的外围信息,但如果开发者想要理解C#模块(或者任何基于CLI的程序)的内部工作原理,但又拿不到源代码,那么只要作者没有使用混淆器,理解这样的CIL代码清单还是比较容易的。事实上,一些免费工具(如Red Gate Reflector、ILSpy、JustDecompile、dotPeek和CodeReflect)可以将CIL自动反编译成C#。

1.4 小结

本章对C#进行了初步介绍。通过本章的学习,你熟悉了基本的C#语法。由于C#与C++风格语言的相似性,本章许多内容可能都是你所熟悉的。然而,C#和托管代码确实有一些独特性,比如会编译成CIL等。C#的另一个关键特征在于它是完全面向对象的。即使是在控制台上读取和写入数据这样的事情,也是面向对象的。面向对象是C#的基础,这一点将贯穿全书。

下一章将探讨C#语言中的基本数据类型,并讨论如何将这些数据类型应用于操作数来构成表达式。


[1] 第一次C#设计会议在1998年举行。

[2] 如果不知道Inigo Montoya是谁,请找《公主新娘》(The Princess Bride)这部电影来看一看。

[3] 设计C#时,语言的创建者从C/C++规范中删除了他们不喜欢的一些特性,同时创建了他们喜欢的一些特性。开发组还有其他语言的资深专家。

[4] 也可以使用.NET Core(.NET的跨平台的实现,可以通过http://dotnet.github.io/core获得)进行编译。虽然我个人很乐意给出在其他平台下使用的命令,但这样会使读者分心。请参见附录A了解.NET Core的详情或者访问http://itl.tc/GettingStartedWithDNX。

[5] 例如,在C# 2.0设计之初,语言的设计者们将yield指定成关键字。在微软发布的C# 2.0编译器的alpha版本中 (该版本分发给了数千名开发人员),yield以一个新关键字的身份存在。然而,语言的设计者最终选择使用 yield return而不是yield,从而避免将yield作为新关键字。除非与return连用,否则yield没有任何特殊意义。

[6] 偶尔也有不兼容的情况,例如,C# 2.0要求为using语句提供的对象必须实现IDisposable接口,而不能只是实 现Dispose()方法。还有一些极少见的泛型表达式,如F(G<A,B>(7)),在C# 1.0中代表F( (G<A), (B>7) ),而 在C# 2.0中代表调用泛型方法G<A,B>,传递实参7,结果传给F。

[7] 有两种单词缩写,一种是“Abbreviation”,如Professor缩写为Prof.;另一种是“Contraction”,如Doctor缩写 为Dr。——译者注

[8] 某些语言的文字编码要用两个16位值表示。第一个代码值称为“高位代理项”(high surrogate),第二个代码 值称为“低位代理项”(low surrogate)。在代理项的帮助下,Unicode可以表示100多万个不同的字符。美国 和欧洲地区很少使用代理项,东亚各国则很常用。——译者注

[9] 即literal,是指以文本形式嵌入的数据。literal有多种译法,没有一种占绝对优势。最典型的译法是“字面 量”、“文字常量”和“直接量”。本书采用前者。——译者注

[10] CIL的第三种说法是Microsoft IL (MSIL)。本书使用CIL一词,因其是CLI标准所采纳的。C#程序员交流时 经常使用IL一词,因为他们都假定IL是指CIL而不是其他中间语言。

[11] 参见Miller, J.和S. Ragsdale编著的The Common Language Infrastructure Annotated Standard(Addison-Wesley, 2004)。

目录

  • 版权
  • 内容提要
  • 版权声明
  • 献给
  • 前言
  • 致谢
  • 第1章 C#概述
  • 第2章 数据类型
  • 第3章 操作符和控制流
  • 第4章 方法和参数
  • 第5章 类
  • 第6章 继承
  • 第7章 接口
  • 第8章 值类型
  • 第9章 良构类型
  • 第10章 异常处理
  • 第11章 泛型
  • 第12章 委托和Lambda表达式
  • 第13章 事件
  • 第14章 支持标准查询操作符的集合接口
  • 第15章 使用查询表达式的LINQ
  • 第16章 构建自定义集合
  • 第17章 反射、特性和动态编程
  • 第18章 多线程处理
  • 第19章 线程同步
  • 第20章 平台互操作性和不安全的代码
  • 第21章 CLI
  • 附录A 下载和安装C#编译器与CLI平台
  • 附录B 井字棋源代码清单
  • 附录C 使用TPL和C# 6.0之前的多线程处理模式(网上下载)
  • 附录D C# 6.0的async/await模式之前的计时器(网上下载)

相关技术

推荐用户