《Csharp 8.0 and .NET Core 3.0 – Modern Cross-Platform Development 4th Edition》第二章
不涉及高级和晦涩的主题,例如 ref 局部变量重新分配和值类型的引用语义。
用反射获取类型数量:
using System.Linq;
using System.Reflection;
namespace ConsoleApp
{
class Console1
{
static void Main(String[] args)
{
foreach (var r in Assembly.GetEntryAssembly()
.GetReferencedAssemblies())
{
var a = Assembly.Load(new AssemblyName(r.FullName));
int methodCount = 0;
foreach (var t in a.DefinedTypes)
{
methodCount += t.GetMethods().Count();
}
Console.WriteLine(
"{0:N0} types with {1:N0} methods in {2} assembly.",
arg0: a.DefinedTypes.Count(),
arg1: methodCount,
arg2: r.Name
);
}
}
}
}
输出:
0 types with 0 methods in System.Runtime assembly.
108 types with 1,141 methods in System.Linq assembly.
46 types with 670 methods in System.Console assembly.
…
定义一些来自其他 assembilies 的类型的变量,这些 assembilies 将被加载到我们的应用中。将以下变量定义在 Main 函数的开头,然后重新运行上面的程序:
System.Data.DataSet ds;
System.Net.Http.HttpClient client;
得到:
0 types with 0 methods in System.Runtime assembly.
400 types with 7,038 methods in System.Data.Common assembly.
426 types with 4,613 methods in System.Net.Http assembly.
108 types with 1,141 methods in System.Linq assembly.
46 types with 670 methods in System.Console assembly.
…
我们总在与数据打交道,数据在程序中往往存储在变量中,它们占用了内存并会在程序结束时释放内存。
我们在使用变量时,应该考虑两件事:1. 它用了多少内存;2. 它将被如何处理。
我们通过选择合适的类型来控制这两件事情。
小内存的类型并不一定比大内存的类型更快,比如 16 bit 的整数在 64 位系统上没有 64 bit 的证书快。
小写开头驼峰法(Camel case),用于局部变量;私有字段;
大写开头(Title case),用于类型、非私有字段和其他成员如函数。
在 C#6.0 中引入了关键字
nameof
来查看变量名。
double heightInMetres = 1.88;
Console.WriteLine($"The variable {nameof(heightInMetres)} has the value {heightInMetres}.");
Literal values
首先是字符,单个字母,用 char 类型存储,通过 加单引号表示对应的字面值,例如:
char letter = 'A';
char digit = '1';
char symbol = '$';
多个字符组成字符串,使用 string 类型存储,双引号表示对应的字面值,例如:
string firstName = "Bob";
字符串还有一些转义字符时,但是只想当作普通字符串用,则需要 @ 作为字符串前缀关闭字符串的转义:
string filePath = @"C:\televisions\sony\bravia.txt";
之前还演示过格式字符串:
string s = $"The variable {nameof(heightInMetres)} has the value {heightInMetres}.";
uint naturalNumber = 23;
int integerNumber = -23;
// F 后缀表示单精度浮点数字面值
float realNumber = 2.3F;
double anotherRealNumber = 2.3;
…
在 C#7.0 中允许使用 _
来分割整数(无论什么进制),如 1_000_000
。
0b
开头:0b_0001_1110
;0x
开头:0x_001E_8480
;decimal 为 16 bytes 的小数,范围没有 double 大,但是可以进行精准的小数等号比较。
注意!double 受精度限制,不能与其他值进行等号比较。
double 有一些特殊值:double.NaN
表示非数字,double.Epsilon
是可以用一个 double 表示的最小的正数。double.Infinity
表示无限大数。
浮点数大都采用 IEEE 754 标准设计浮点数。
C# 有操作符 sizeof()
来获取一个类型使用的 byte 数;一些类型有 MaxValue
和 MinValue
成员,来获取该类型可表达的最大和最小值。
一个例子来查看这些类型的信息:
Console.WriteLine($"int uses {sizeof(int)} bytes and can store numbers in the range {int.MinValue:N0} to {int.MaxValue:N0}.");
Console.WriteLine($"double uses {sizeof(double)} bytes and can store numbers in the range {double.MinValue:N0} to {double.MaxValue:N0}.");
Console.WriteLine($"decimal uses {sizeof(decimal)} bytes and can store numbers in the range {decimal.MinValue:N0} to {decimal.MaxValue:N0}.");
输出:
int uses 4 bytes and can store numbers in the range -2,147,483,648 to 2,147,483,647.
double uses 8 bytes and can store numbers in the range -179,769,313,486,231,570,814,527,423,731,704,356,798,070,567,525,844,996,598,917,476,803,157,260,780,028,538,760,589,558,632,766,878,171,540,458,953,514,382,464,234,321,326,889,464,182,768,467,546,703,537,516,986,049,910,576,551,282,076,245,490,090,389,328,944,075,868,508,455,133,942,304,583,236,903,222,948,165,808,559,332,123,348,274,797,826,204,144,723,168,738,177,180,919,299,881,250,404,026,184,124,858,368 to 179,769,313,486,231,570,814,527,423,731,704,356,798,070,567,525,844,996,598,917,476,803,157,260,780,028,538,760,589,558,632,766,878,171,540,458,953,514,382,464,234,321,326,889,464,182,768,467,546,703,537,516,986,049,910,576,551,282,076,245,490,090,389,328,944,075,868,508,455,133,942,304,583,236,903,222,948,165,808,559,332,123,348,274,797,826,204,144,723,168,738,177,180,919,299,881,250,404,026,184,124,858,368.
decimal uses 16 bytes and can store numbers in the range -79,228,162,514,264,337,593,543,950,335 to 79,228,162,514,264,337,593,543,950,335.
再次说明:decimal 为 16 bytes 的小数,范围没有 double 大,但是可以进行精准的小数等号比较。注意!double 受实现的精度限制,不能与其他值进行等号比较。
布尔值:bool happy = true;
有一种特殊类型叫 object
,它能够存储任何类型的数据,但是灵活性的代价是更混乱的代码和可能较差的性能,所以应该尽可能避免。
object height = 1.88;
object name = "Amir";
Console.WriteLine($"{name} is {height} metres tall.");
//int length1 = name.Length; // 编译器报错:error CS1061: “object”未包含“Length”的定义
int length2 = ((string)name).Length; // 转换到 string 类型在使用对应的方法
Console.WriteLine($"{name} has {length2} characters.");
dynamic types
另一种能够存储任意类型数据的类型叫 dynamic
,它的灵活性的代价是性能。dynamic
关键字自 C#4.0 引入。区别于 object
,dynamic
能够直接调用对应的成员而无需显式类型转换。
dynamic anotherName = "Ahmed";
int length = anotherName.Length;
Console.WriteLine($"{anotherName} has {length} characters.");
在 VSCode 中编写时发现,dynamic
类型的数据无法得到 IntelliSense
智能感知,这是因为编译器无法在构建时检测 dynamic
的具体类型。取而代之的是,CLR 将会在运行时检查 dynamic
类型的成员,并可能再找不到成员时抛出异常。
是指在函数中定义的变量,只在函数执行时存在,函数返回则释放内存。
准确的说:值类型会释放,但是引用类型必须等待垃圾回收(garbage collection)。
可以使用 var
来进行类型推断,根据等号后面的字面值类型判断变量的类型,具体地:
int
,如果有 L
后缀,则为 long
,double
,如果有 F
后缀,则为 float
,如果有 M
后缀则为 decimal
类型。char
类型,双引号括住表示 string
类型true
和 false
表示 bool
类型。良好实践:一般来说不建议使用 var ,因为这可能导致难以阅读;除非类型已经相当明显了。举例:
//好的使用,避免重复类型
var xml = new XmlDocument();
XmlDocument xml2 = new XmlDocument();
//不好的使用,不容易让人知道类型
var file1 = File.CreateText(@"C:\something.txt");
StreamWriter file2 = File.CreateText(@"C:\something.txt");
…
除了 string
外的大多数原始类型都是值类型(value types),这意味着它们必须有一个值。可以使用 default()
操作符确定一个类型的默认值。
string
是一个引用类型(reference type),这意味着 string
变量包含一个值(value
)的内存地址而不是值(value
)本身。一个引用类型可以有一个 null
值,表示一个变量还没有引用任何东西。null
是所有引用变量的默认值。
我们可以通过代码探究默认值:
Console.WriteLine($"default (int) = {default(int)}");
Console.WriteLine($"default (bool) = {default(bool)}");
Console.WriteLine($"default (DateTime) = {default(DateTime)}");
Console.WriteLine($"default (string) = {default(string)}");
输出:
default (int) = 0
default (bool) = False
default (DateTime) = 0001/1/1 0:00:00
default (string) =
例子:
string[] names;
names = new string[4];
names[0] = "Kate";
names[1] = "Jack";
names[2] = "Rebecca";
names[3] = "Tom";
for (int i = 0; i < names.Length; i++)
{
Console.WriteLine(names[i]);
}
数组在内存分配时始终具有固定大小,因此需要在实例化它们之前决定要存储多少项。
collections
是一个更灵活的数组,之后介绍。
您现在已经了解了如何在变量中存储原始值(例如数字)。但是如果变量还没有值怎么办?我们如何表明这一点? C#有空值(null
)的概念,它可以用来表示变量还没有被设置。
默认情况下,像 int
和 DateTime
这样的值类型必须始终有一个值,因此得名。但是,比如有时读取数据库时可能得到一个空的值,为了便捷允许一个值类型为 null
,我们称之为可空值类型(nullable value type
)。
声明可空值类型时在类型后面加一个 ?
。例如:
//int thisCannotBeNull = 4;
//thisCannotBeNull = null; //编译报错
int? thisCouldBeNull = null;
Console.WriteLine(thisCouldBeNull);
Console.WriteLine(thisCouldBeNull.GetValueOrDefault());
thisCouldBeNull = 7;
Console.WriteLine(thisCouldBeNull);
Console.WriteLine(thisCouldBeNull.GetValueOrDefault());
输出:
0
7
7
null
什么都不输出
在许多语言中,空值的使用非常普遍,以至于许多经验丰富的程序员从不质疑它存在的必要性。但在很多情况下,如果变量不允许有空值,我们可以编写更好、更简单的代码。
C# 8.0 中对该语言最重要的变化是引入了可为 null 和不可为 null 的引用类型。 在 C# 8.0 中,可以通过设置文件级或项目级选项来将引用类型配置为不再允许 null 值。
注意:使能该特性意味着引用类型和值类型一样不可空,但是可以使用
?
后缀使之可空。
或
enable #nullable enable
是指默认不可空,而不是可空。具体什么意思还不懂可以往下看。
项目级设置可空,需要在项目文件 xx.csproj
中添加:
<PropertyGroup>
<Nullable>enableNullable>
PropertyGroup>
文件级设置,需要在文件顶部加入:
#nullable disable
#nullable enable
当项目级和文件设置可控为 enable 时:
enable
#nullable enable
我们定义如下类:
class Address
{
public string? Building;
public string Street;
public string City;
public string Region;
}
会发现后面的三个字段:Street、City、Region 都被提出了警告:
在退出构造函数时,不可为 null 的 字段“Street”必须包含非 null 值。请考虑将 字段 声明为可以为 null。
在 Main 函数中定义一个实例并设置属性:
var address = new Address();
address.Building = null;
address.Street = null;
address.City = "London";
address.Region = null;
Street
和 Region
给出警告:
无法将 null 字面量转换为非 null 的引用类型。
因此,这就是新语言功能被命名为可为空引用类型的原因。从 C# 8.0 开始,未修饰的引用类型可以变为不可为 null,并且使用与值类型相同的语法使引用类型可以为 null。
如果是
#nullable disable
呢?
那么上述代码只会有一个警告,来自 public string? Building;
:
只能在 "#nullable" 注释上下文内的代码中使用可为 null 的引用类型的注释。
在.NET8.0中,该特性默认开启。
…
如果不检查 null,就可能引发 NullReferenceException
异常。
检查 null 的代码如下所示:
// check that the variable is not null before using it
{if (thisCouldBeNull != null)
// access a member of thisCouldBeNull
int length = thisCouldBeNull.Length; // could throw exception
...
}
如果你尝试使用一个可能为空的变量的成员,使用 null 条件操作符 ?
,如下所示:
string authorName = null;
// 下面这行会抛出 NullReferenceException
int x = authorName.Length;
// 不会抛出异常了, null 被赋给 y
int? y = authorName?.Length;
有时,您想要将变量赋给结果或使用替代值,例如 3(如果变量为 null)。可以使用 null-coalescing 运算符 ??
:
// 如果 authorName?.Length 是空,那么就用 3 代替
var result = authorName?.Length ?? 3;
Console.WriteLine(result);
…
关于该运算符的更多资料:https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/null-coalescing-operator
控制台应用程序是基于文本的,并在命令行上运行。它们通常执行需要编写脚本的简单任务,例如编译文件或加密配置文件的一部分。
如果像创建 F# 语言的控制台程序,可以使用:
dotnet new console -lang "F#" -name "ExploringConsole"
打印信息时,也可以使用 Console.Write
函数,它不会在最后输出换行符。
Formatting using numbered positional arguments
生成格式化字符串的一种方法是使用数字化位置参数。
该特性被像 Write
和 WriteLine
的函数支持,如果函数不支持,可以用 string
的 Format
方法。
例如:
int numberOfApples = 12;
decimal pricePerApple = 0.35M;
Console.WriteLine(format: "{0} apples costs {1:C}",arg0: numberOfApples,arg1: pricePerApple * numberOfApples);
string formatted = string.Format(format: "{0} apples costs {1:C}",arg0: numberOfApples,arg1: pricePerApple * numberOfApples);
//WriteToFile(formatted); // WriteToFile并不存在,仅作演示
Formatting using interpolated strings
C#6.0 及之后版本有一个方便的特性叫内插字符串(interpolated strings)。一个使用 $
作为前缀的 string
可是使用 大括号将表达式的值输出到字符串对应的位置。
例如:
Console.WriteLine($"{numberOfApples} apples costs {pricePerApple *numberOfApples:C}");
对于短字符串来说这样很好理解。但是长字符串不容易换行,难以阅读,这时候可以用前面说的数字化位置参数。
可以使用逗号或冒号后面的格式字符串来格式化变量或表达式。
一个 N0
格式字符串表示一个有着千位分隔符且没有小数点的数字;c 格式字符串表示货币。货币格式化将有当前线程(地区?)确定。例如,如果您在英国的 PC 上运行此代码,您将得到以逗号作为千位分隔符的英镑,但如果您在德国的 PC 上运行此代码,您将得到以点作为千位分隔符的欧元。
格式项的完整语法是:
{index [, alignment] [ : formatString]}
每个格式化项可以有一个对齐,在字符宽度内左对齐或右对齐。对齐数值(alignment values)是一个整数,正整数表示右对齐,而负整数表示右对齐。
例子:
string applesText = "Apples";
int applesCount = 1234;
string bananasText = "Bananas";
int bananasCount = 56789;
Console.WriteLine(
format: "{0,-8} {1,6:N0}",
arg0:"Name",
arg1:"Count"
);
Console.WriteLine(
format: "{0,-8} {1,6:N0}",
arg0:applesText,
arg1:applesCount
);
Console.WriteLine(
format: "{0,-8} {1,6:N0}",
arg0:bananasText,
arg1:bananasCount
);
输出:
Name Count
Apples 1,234
Bananas 56,789
使用 ReadLine
方法获取用户输入,用户输入完毕后按下回车,该函数返回一个 string
。
例子:
Console.Write("Type your first name and press ENTER: ");
string firstName = Console.ReadLine();
Console.Write("Type your age and press ENTER: ");
string age = Console.ReadLine();
Console.WriteLine(
$"Hello {firstName}, you look good for {age}."
);
输出:
Type your first name and press ENTER: Peter
Type your age and press ENTER: 24
Hello Peter, you look good for 24.
和 C++ 的名称空间作用类似。
System
空间限制被默认导入。
C#6.0之后, using 声明用来进一步简化我们的代码。
using static System.Console;
这样,我们就不用写 Console 了。
使用函数 ReadKey
获取按键,按下任意按键立刻返回 string
例如:
Write("Press any key combination: ");
ConsoleKeyInfo key = ReadKey();
WriteLine();
WriteLine("Key: {0}, Char: {1}, Modifiers: {2}",
arg0: key.Key,
arg1: key.KeyChar,
arg2: key.Modifiers);
运行后,按下 k 键:
Press any key combination: k
Key: K, Char: k, Modifiers: 0
按下 Shift + k:
Press any key combination: K
Key: K, Char: K, Modifiers: Shift
按下 F12:
Press any key combination:
Key: F12, Char: , Modifiers: 0
命令行参数通过空格分割,其他字符(如连字符和冒号)被视为参数值的一部分。要在参数值中包含空格,请将参数值用单引号或双引号引起来。
举例:
using System;
using static System.Console;
namespace Arguments
{
class Program
{
static void Main(string[] args)
{
WriteLine($"There are {args.Length} arguments.");
foreach (string arg in args)
WriteLine(arg);
}
}
}
}
运行时使用:dotnet run firstarg second-arg third:arg "fourth arg"
,得到输出:
There are 4 arguments.
firstarg
second-arg
third:arg
fourth arg
比如,我们想通过命令行参数设置输出窗口的背景颜色、前景颜色、宽度、高度。导入的 System 名称空间包含 ConsoleColor 和 Enum 类型:
if (args.Length < 4)
{
WriteLine("You must specify two colors and dimensions,e.g.");
WriteLine("dotnet run red yellow 80 40");
return; // stop running
}
ForegroundColor = (ConsoleColor)Enum.Parse(
enumType: typeof(ConsoleColor),
value: args[0],
ignoreCase: true);
BackgroundColor = (ConsoleColor)Enum.Parse(
enumType: typeof(ConsoleColor),
value: args[1],
ignoreCase: true);
WindowWidth = int.Parse(args[2]);
WindowHeight = int.Parse(args[3]);
然后运行以下命令即可改变颜色:
dotnet run red yellow 50 10
尽管编译器没有给出错误或警告,但在运行时,某些 API 调用可能会在某些平台上失败。尽管在 Windows 上运行的控制台应用程序可以更改其大小,但在 macOS 上却不能。
遇到这种问题,我们可以用异常处理来解决。例如:
try
{
WindowWidth = int.Parse(args[2]);
WindowHeight = int.Parse(args[3]);
}
catch (PlatformNotSupportedException)
{
WriteLine("The current platform does not support changing the size of a console window.");
}