C# 8.0和.NET Core 3.0高级编程 分享笔记二:编程基础第一部分

基础部分被我分为了2篇,因为实在太多了,但是每一个知识点我都不舍得删除,所以越写越多,这一篇博客整理了4个夜晚,内容有点多建议慢慢看。本章涵盖以下主题:

  • 介绍C#

  • 理解C#的基础知识

  • 使用变量

  • 处理空值

    下一章进一步探索控制台应用程序。

    2.1) 介绍C#

    1.C#1.0

    C#1.0于2002年发布,其中包括了静态类型的面向对象编程语言的所有重要特性,本书第一大部分所有章节都会介绍这些特性。

    2.C#2.0

    C#2.0是在2005年发布的,重点是使用泛型实现强数据类型,以提高代码性能、减少类型错误,其中包含的主题如表2.1所示。

    表2.1 C#2.0中包含的主题

功能 涉及的章节 主题
可空的值类型 第2章 使值类型为空
泛型 第6章 使类型与泛型更加可重用

3.C#3.0

C#3.0是2007年发布的,重点是使用语言集成查询(LINQ)以及匿名类型和lambda表达式等支持声明式编程,其中包含的主题如表2.2表示。

表2.2 C#3.0中包含的主题

功能 涉及的章节 主题
隐式类型的局部变量 第2章 推断局部变量的类型
LINQ 第12章 所有的主题详见第12章

4.C#4.0

C#4.0是在2010年发布的,重点是利用F#和Python等动态语言改进互操作性,其中包含的主题如表2.3所示:

表2.3 C#4.0中包含的主题

功能 涉及的章节 主题
动态类型 第2章 dynamic类型
命名/可选参数 第5章 可选参数和命名参数

5.C#5.0

C#5.0发布于2012年,重点是简化异步操作支持,从而在编写类似于同步语句的语句时自动实现复杂的状态机,其中包括的主题如表2.4所示。

表2.4 C#5.0中包含的主题

功能 涉及的章节 主题
简化异步任务 第13章 理解async和await

6.C#6.0

C#6.0于2015年发布,专注于对语言的细微改进,其中包括的主题如表2.5所示。

表2.5 C#6.0中包含的主题

功能 涉及的章节 主题
静态导入 第2章 简化了控制台的使用
内插字符串 第2章 向用户显示输出
表达式体成员 第5章 定义只读属性

7.C#7.0

C#7.0是在2017年3月发布的,重点是添加功能语言特性,如元组和模式匹配,还对语言做了细微改进,其中包含的主题如表2.6所示。

表2.6 C#7.0中包含的主题

功能 涉及的章节 主题
二进制字面量和数字分隔符 第2章 存储整数
模式匹配 第3章 利用if语句进行模式匹配
out变量 第5章 控制参数的传递方式
元组 第5章 将多个值于元组组合在一起
局部函数 第6章 定义局部函数

8C#7.1

C#7.1是在2017年8月发布的,重点是对语言做了细微改进,其中包含的主题如表2.7所示。

表2.7 C#7.1中包含的主题

功能 涉及的章节 主题
默认字面量表达式 第5章 使用默认字面量设置字段
推断元组元素的名称 第5章 推断元组名称
async Main 第13章 改进对控制台应用程序的响应

9.C#7.2

C#7.2是在2017年11月发布的,重点是对语言做了席位改进,其中包含的主题如表2.8所示。

表2.8 C#7.2中包含的主题

功能 涉及的章节 主题
数字字面量中的前导下划线 第2章 存储整数
非追踪的命名参数 第5章 可选参数和命名参数
私有保护访问修饰符 第5章 理解访问修饰符
可以使用元组类型测试==和!= 第5章 比较元组

10.C#7.3

C#7.3于2018年5月发布,主要关注性能导向型的安全代码,并且改进了ref变量、指针和stackalloc。这些都是高级功能,对于大多数开发人员来说很少使用,因此不涉及它们。
如果感兴趣可以通过C#7.3网址访问。

11.C#8.0

C#8.0于2019年9月发布,主要关注与空处理相关的语言的重大变化,其中包含的主题如表2.9所示。

表2.9 C#8.0中包含的主题

功能 涉及的章节 主题
可空引用类型 第2章 使引用类型为空
switch表达式 第3章 使用switch表达式简化switch语句
默认的接口方法 第6章 了解默认的接口方法

2.1发现C#编译器版本

在C#7.x中,微软决定加快语言的发布节奏-发布次要版本号,也成为点发布。
.NET 语言编译器(对于C#、Visual Basic和F#也称为Roslyn)是作为.NET Core SDK的一部分发布的。要使用特定版本的C#,就必须至少安装对应版本的.NET Core SDK, 如表2.10所示。

表2.10 不同C#版本对应的.NET Core SDK版本

.NET Core SDK版本 Roslyn 版本 C#版本
1.04 2.0~2.2 7.0
1.1.4 2.3和2.4 7.1
2.1.2 2.6和2.7 7.2
2.2.200 2.8~2.10 7.3
3.0 3.0~3.3 8.0

通过以下链接查看版本列表
查看可用的C#编译器版本:
1)启动Visual Studio Code。
2)导航到View|Terminal。
3)要确定可以使用哪个版本的.net Core SDK,输入
dotnet --version

2.1.3启用特定的语言版本编译器

如果要使用特定的版本,就需要在项目文件中添加配置元素,比如微软发布了C# 8.1的编译器,并且希望使用C#8.1的新语言特性,那就必须在项目中编辑.csproj并添加配置元素:

  <Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
      <OutputType>Exe</OutputType>
      <TargetFramework>netcoreapp3.0</TargetFramework>
      <LangVersion>8.1</LangVersion>
     </PropertyGroup>
  </Project>

2.2)理解C#的基础知识。

为了学习C#,你需要创建一些简单的应用程序。为了避免过快地提供过多的信息,第一大章主要使用最简单的程序类型:控制台应用程序。本章将创建多个控制台应用程序,每个控制台程序显示C#语言的一个特性。执行以下步骤:
(1)如果已经完成了第一章,那么我们在Code文件夹下由Chapter01子文件夹,如果没用,需要创建Code文件夹。
(2)创建名为Chapter02的子文件夹,在其中再创建名为Basics的子文件夹。
(3)启动Visual Studio Code 并打开Chapter02/Basics文件夹
(4)在Visual Studio Code 中导航到View|Terminal,输入以下命令:
dotnet new console
(5)在资源管理器中单击Program.cs文件,然后单击右下角的Yes以添加缺少的必须资产。

2.2.1)了解C#语法。

C#语法包括语句和块。要记录代码,可以使用注释。

1.语句

在英语中,人们使用句点来表示句子的结束。句子可以由多个单词和短语组成,单词的顺序是语法的一部分。例如在英语句子the black cat中,形容词black在名词cat之前;而在法语中,含义相同的句子为le chat noir,形容词noir跟在名词chat的后面。从这里可以看出,单词的顺序很重要。
C#用分号表示语句的结束。C#语句可以由多个变量和表达式组成。例如,在下面的C#语句中,totalPrice是变量,而subtotal+salesTax是表达式。
var totalPrice=subtotal+salesTax;
以上表达式由一名为sybtotal的操作数、运算符+和另一个名为salesTax的操作数组成。操作数和运算符的顺序很重要。

2.注释

在编写代码时,可以使用双斜杠//添加注释以解释代码。通过插入//,编译器将忽略//后面的所有内容,直到行尾,如下所示:
// sales tax must be added to the subtotal

var totalPrice = subtotal+ salesTax;

如果按Ctrl+K+C组合键来添加注释或按Ctrk+K+U组合键来删除注释,那么Visual Studio Code 将在当前选中行的开头添加或删除注释用的双斜杠。在macOS中,对应的方法时按Cmd键二不是Ctrl键。
要编写多行注释,请在注释的开头使用/,在结尾使用/,如下所示:
/*
The is a multi-line
comment.
*/

3.块

C#使用花括号{}表示代码块(简称块)。块以声明开头,以指示正在定义什么。例如,块可以定义名称空间、类、方法或语句,稍后详细介绍。
在当前项目中,请注意C#语法是使用dotnet CLI工具编写的。在项目模板的语句中添加一些注释,如下所示:

using System;
namespace Basics
{
  class Program
  {
    static void Main(string[] args)
      {
  	// the start of a block
        Console.Writeline("Hello World!"); //a statement
  	  // the end of a block
  	}
  }
}

2.2.2) 了解C#词汇表

C#词汇表由关键字、符号字符和类型组成。
我们看到一些预定义的保留关键字包括using、namespace、class、static、int、string、double、bool、if、switch、break、while、do、for和foreach。符号字符可能包括"、'、+、-、*、/、%、@和$。
默认情况下,Visual Stuodio Code以蓝色显示C#关键字,以便于其他代码区分开来。Visual Studio Code 允许自定义配色方案。执行以下步骤:
(1)在Visual Studio Code中导航到Code|Preferences|color Theme。
(2)选择一种颜色主题。作为参考。
还有一些其他的上下文关键字,他们只在特定的上下文中具有特定的含义。然而,这仍然意味着C#语言中只有大概100个实际的C#关键字。
英语有超过250 000个不同的单词,那么C#怎么可能只有大概100个关键字呢?此外,如果C#仅为英语的0.04%,那么为什么C#会如此难学呢?
人类语言和编程语言之间的关键区别是:开发人员需要能够定义具有新含义的新“单词”,除了C#语言中的大约100个关键词之外,本书还将介绍其他开发人员定义的数十万个“单词”中的一些,你将学习如何 定义自己的“单词”。
PS:全世界的程序员都必须学习英语,因为大多数编程语言使用的都是英语单词,比如Namespace和CLass,有些编程语言使用其他人类语言,如阿拉伯语,但他们很少见。

1.如何编写正确的代码

像记事本这样的纯文本编辑器并不能帮助你写出正确的英语。同样,记事本也不能帮助写出正确的C#代码。
微软的Word软件可以帮助你写英语,Word软件会使用红色波浪线来强调拼写错误,比如icecream应该是ice-cream或ice cream;而用蓝色波浪线强调语法错误,比如句子应该使用大写的首字母。
类似的,Visual Studio Code 的C#扩展可通过突出显示拼写错误(比如方法名WriteLine中的L应该大写)和语法错误(语句必须分号结尾)来帮助你编写C#代码。
C#扩展不断地监视输入的内容,并通过彩色的波浪线高亮显示问题来提供反馈,这与Word软件类似。
下面看看具体是如何运作的。
(1)在Program.cs中,将WriteLine方法中的L改为小写。
(2)删除语句末尾的分号。
(3)导航到View|Problems,注意红色的波浪线出现在错误代码下方,具体细节显示在Problems网格中,
(4)修复编码错误。

2.动词表示方法

在英语中,动词时动作或行动,例如run和Jump。在C#中,动作或行动被称为方法。C#由成千上万个方法可用。在英语中,动词的写法取决于动作发生的事件。例如,jump的过去进行时是was jumping ,现在时是jumps,过去时是jumped,未来时是will jump。
在C#中,像WriteLine这样的方法会根据操作的细节改变调用或执行的方式。这称为重载,第5章详细讨论这个问题。但现在,考虑以下示例:
Console.WriteLine();
Console.WriteLine("Hello Ahmed");
Console.WriteLine("Temperature on {0:D} is {1}° C.",DateTime.Today,23.4);
另一个不同的类比是:有些单词的拼写相同,但根据上下文有不同的含义。

3.名词表示类型、字段和变量

在英语中,名词是指事物的名称。例如Fido是一只狗的名字。
在C#中,等价物是类型、字段和变量。例如,Animal和Car是类型;也就是说它们是用来对事物进行分类的名词。Head和Engine是字段,他们是属于Animal和Car的名词。Fido和Bob是变量,也就是说,它们是指代特定事物的名词。
C#由成千上万种可用的类型,但是注意,这里并没有说“C#中由成千上万中类型”。这种差别很细微,但很重要。C#语言只有一些类型关键字,比如string和int。严格来说C#没用定义任何类型,类似于string的关键字是别名,他们表示运行在C#平台所提供的类型。
你要知道,C#不能单独存在;毕竟,C#是一种运行在不同.NET变体上的语言。理论上,可以为C#编写使用不同平台和底层类型的编译器。实际上,C#的平台是.NET,.NET为C#提供了成千上万种类型,包括System.Int32以及许多更复杂的类型,如System.XmlLinq.XDocument.
注意,术语type(类型)和class(类)很容易混淆.你有没有玩过室内游戏《二十个问题》?在这个游戏中,任何东西都可以归类为动物、素菜或者矿物.在C#中,每种类型都可以归类为类、结构、枚举、接口或者委托。C#关键字string 是类,而int是结构体。因此最好使用术语type指代它们两者。

4.揭示C#词汇表的范围

我们知道C#中大约100个关键字,但是由多少类型呢,我们编写以下代码以便找出简单的控制台应用程序中有但是类型(及方法)可用于C#。
现在不用担心代码是如何工作的,这里使用了一种叫做反射的技术。执行以下步骤。
(1)首先在Program.cs中顶部添加以下代码:

using System.Linq;
using System.Reflection;

(2)在Main方法内删除用于写入Hello World!的语句,并将它们替换为以下代码:

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,arg1:methodCount,arg2:r.Name);
           }
      }

(5)运行上述命令后,你就能看到在最简单的应用程序中可用的类型和方法的实际数量。这里显示的类型和方法的数量可能会根据使用的操作系统而有所不同。
PS D:\Code\Chapter02\Basics> dotnet run
System.RuntimeType[] types with 325 methods in System.Runtime assembly.
System.RuntimeType[] types with 1,068 methods in System.Linq assembly.
System.RuntimeType[] types with 638 methods in System.Console assembly.
(6)在Main方法的顶部添加语句以声明一些变量,如下所示:

      static void Main(string[] args)
      {
          System.Data.DataSet dataSet;
          System.Net.Http.HttpClient client;
        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,arg1:methodCount,arg2:r.Name);
           }
      }

System.RuntimeType[] types with 325 methods in System.Runtime assembly.
System.RuntimeType[] types with 6,763 methods in System.Data.Common assembly.
System.RuntimeType[] types with 4,167 methods in System.Net.Http assembly.
System.RuntimeType[] types with 1,068 methods in System.Linq assembly.
System.RuntimeType[] types with 638 methods in System.Console assembly.
通过声明要在其他程序集中使用类型的变量,应用程序将加载这些程序集,从而允许代码查看其中的所有类型和方法。编译器会警告存在未使用的变量,但这不会 阻止代码的运行。
现在你可以更好的理解为什么学习C#是一大挑战,因为有太多的类型和方法需要学习,方法知识类型可以拥有的成员的类别,而其他程序员正在不断地定义新成员!

2.3)使用变量

所有应用程序都要处理数据。数据都是先输入,在处理,最后输出。数据通常来自文件、数据库或用户输入,可以临时放入变量中,这些变量存储在运行程序的内存中。当程序结束时,内存中的数据会丢失。数据通常输出到文件和数据库中,抑或输出到屏幕或打印机。当使用变量时,首先应该考虑它在内存中占用了多少空间,其次考虑它的处理速度有多块。
变量可通过选择合适的类型来控制。可以将简单的常见类型(如int和double)视为不同大小的存储盒,其中较小的存储和占用的内存少,但处理速度可能没用那么快;例如,在64位系统中,添加16位数字的速度,可能不如添加64位数字的速度快,这些盒子有的可能堆放在附近,有的可能被扔到更远的一大堆盒子里。

2.3.1 命名和赋值

事物都有命名约定,最好遵循这些约定,如表2.12所示。

表2.12命名约定

命名约定 示例 适用场合
驼峰样式 Cost、orderDetail、dateOfBirth 局部变量、私有字段
标题样式 String、Int32、Cost、DateOfBirth、Run 类型、非私有字段以及其他成员(如方法)

遵循一组一致的命名约定,将使代码更容易被其他开发人员理解以及将来的自己理解。
以下代码块显示了一个声明已命名的局部变量并使用=符号为之赋值的示例。注意,可以使用C#6.0中引入的关键字nameof来输出变量的名称。

double heightInMetres=1.88;
Console.WriteLine($"The variable {nameof(heightInMetres)} has the value {heightInMetres}.");

字面值

在给变量赋值时,赋予的经常(但不总是)是字面值。什么是字面值呢?字面值是表示固定值的符号。数据类型的字面值有不同的表示法,接下来的内容中包含了使用字面符号为变量赋值的示例。

2.3.2 存储文本

对于一些文本,比如单个字母(如A),可存储为char类型,并在字面值的两边使用单引号来赋值,也可以直接赋予函数调用的返回值,如下所示:

 char letter='A';
 char dight='1';
 char symbol='$';
 char userChoice=GetKeystroke();
 private static char GetKeystroke()
 {
   return 'S';
 }
//对于另外一些文本,比如多个字母(如Bob),可存储为字符串类型,并在字面值的两边使用双引号进行赋值,同时可也直接赋予为函数调用的返回值,如下所示:
   string firstNmae="Bob";
  =string lastName="Smith";
   string phoneNumber="(215) 555-4256";
   string address=GetAddressFromDatabase(id:563);
   private static string GetAddressFromDatabase(int id)
   {
       return "Beijing ChangPing Beiqijia";
   }

理解逐字字符串
在字符串变量中存储文本时,可以包括转义序列,转义序列使用反斜杠表示特殊字符,如制表符和新行,如下所示:

 string fullNameWithTabSeparator="Bob\tSmith";
 //但是如果要将路径存储到文件中,并且路径中有文件夹的名称以t开头,如下所示:
 string filePaht="C:\televisions\sony\bravia.txt";
 //编译器就会把\t转换成制表符,这显然是错误的,解决办法时逐字符串必须加上@符号作为前缀,如下所示:
string filePathTwo=@"c:\televisions\sony\bravia.txt";

下面进行总结。
1)字面字符串:用双引号括起来的一些字符。它们可以使用转义字符\t作为制表符
2)逐字字符串:以@为前缀的字面字符串,以禁用转义字符,因此反斜杠就是反斜杠。
3)内插字符串:以$为前缀的字面字符串,以支持嵌入式的格式化变量。

2.3.3 存储数字

数字时细微进行算术计算的数据,例如,电话号码不是数字。要决定是否应该将变量存储为数字,请考虑是需要对数字执行算术运算,还是数据应包含圆括号或连字符等非数字字符。以便将数字格式化为(414)555-1234. 在本例中,数字是字符序列,因此应该存储为字符串。
数字可以是自然数,如42,用于计数;它们也可以是负数,如-42(也称之为整数);或者,它们可以是实数,例如3.9(带小数部分),在计算中称为单精度浮点数或双精度浮点数。
下面探讨数字。执行以下步骤:
(1)在Chapter02文件夹中创建一个名为Numbers的新文件夹。

(2)在Visual Studio Code 中打开Numbers文件夹。

(3)在终端使用dotnet new console命令创建一个新的控制台应用程序。

(4)在Main方法内部输入以下语句,以使用不同的数据类型声明一些数字变量:

 uint naturalNumber=23;
 int integerNumber=-23;
 float realNumber=2.3F;
 double anotherRealNumber=2.3;

1.存储整数

计算机把所有东西都存储为位。位的值不是0就是1。这就是所谓的二进制数字系统。人类使用的是十进制数字系统。

十进制数字系统也称为以10为基数的系统,但一些其他的数字基数系统在科学、工程和计算领域也很受欢迎。二进制数字系统以2为基数,也就是说只有两个基数:0和1。

​ ####表2.13计算机如何存储数字10

128 64 32 16 8 4 2 1
0 0 0 0 1 0 1 0

十进制数字10在二进制中表示为00001010。

C#7.0及更高版本中的两处改进是使用下划线_作为数字分隔符以及支持二进制字面值。可以在数字字面值(包括十进制、二进制和十六进制表示法)中插入下划线,以提高可读性。例如可以将十进制数字100 000 写成1_000_000。

二进制记数法以2为基数,只使用1和0,数字字面值的开头是0b。十六进制计数法以16为基数,使用的是09和AF,数字字面值的开头是0x。

下面在Main方法的底部输出如下语句,使用下划线分割符声明一些数字变量:

 int decimalNotation=2_000_000;
 int binaryNotation=0b_0001_1110_1000_0100_1000_0000;
 int hexadecimalNotation=0x_001E_8480;
 Console.WriteLine($"{decimalNotation==binaryNotation}");
 Console.WriteLine($"{decimalNotation==hexadecimalNotation}");

结果表明三个数字是相同的,输出结果是:

True

True

计算机总是可以使用int类型以及其他兄弟类型(如long和short)精确地表示整数。

2.存储实数

计算机并不能总是精确地表示浮点数。float和double类型使用单精度和双精度浮点数存储实数。

大多数编程语言都实现了IEEE浮点运算标准。IEEE754是电气和电子工程师协会(IEEE)与1985年建立的浮点运算技术标准。

浮点数入门教程

表2.14显示了计算机如何用二进制计数法表示数字12.75。

2.14 计算机如何存储数字12.75

128 64 32 16 8 4 2 1 . 1/2 1/4 1/8 1/16
0 0 0 0 1 1 0 0 . 1 1 0 0

所以十进制数字12.75 在二进制中表示为00001100.1100。可以看到,数字12.75可以用位精确的表示。然而,有些数字不能用位精确的表示,稍后探讨这个问题。

3.编写代码以探索数字的大小

C#提供的名为sizeof()的操作符可返回类型在内存中使用的字节数。有些类型又名为MinValue和MaxValue的成员,它们返回可以存储在类型变量中的最小值和最大值。现在我们将使用这些特性创建一个控制台应用程序来研究数字类型。

(1)在Main方法的内部输入如下语句,显示三种数字数据类型的大小:

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 number in the range{double.MinValue:N0} to {double.MaxValue:N0}.");
Console.WriteLine($"decimal uses {sizeof(decimal)} bytes and can store number in the range{decimal.MinValue:N0} to {decimal.MaxValue:N0}.");

int变量使用4字节的内存,可以存储正数和负数。double变量使用8个字节的内存,因而可以存储更大的值!decimal变量使用16字节的内存,虽然可以存储较大的数字,但却不像double类型那么大。我们比较double和decimal类型。

现在来编写一段代码比较double和decimal的值。尽管代码并不难理解,但我们现在不要担心语法。

(1)在前面的语句中,声明两个double 变量,将它们相加并与预期结果进行比较,然后将结果写入控制台,如下所示:

 		double a=0.1;
        double b=0.2;
        if(a+b==0.3)
        {
            Console.WriteLine($"{a}+{b} equals 0.3");
        }
        else
        {
            Console.WriteLine($"{a}+{b} does Not equal 0.3");
        }

(2)运行控制台应用程序并查看结果,如下所示:

0.1+0.2 does NOT equal 0.3

double类型不能保证值是精确的,因为有些数字不能表示为浮点数。

关于为什么0.1不存在于浮点数中的原因。点击这个地址,或者看我下面的分析。

Why 0.1 Does Not Exist In Floating-Point - Exploring Binary

根据经验,应该只在准确性不重要时使用double类型,特别是在比较两个数字的相等性时。例如,当测量一个人的身高时。

上述问题可以通过计算机如何存储数字0.1或0.1的倍数来说明。要用二进制表示0.1,计算机需要在1/16列存储1、在1/32列存储1、在1/256列存储1、在1/512列存储1,以此类推。于是小数中的数字0.1是0.00011001100110011……

表2.15 数字0.1的存储

4 2 1 . 1/2 1/4 1/8 1/16 1/32 1/64 1/128 1/256 1/512
0 0 0 . 0 0 0 1 1 0 0 1 1

永远不要使用==比较两个double值。在第一次海湾战争期间,美国爱国者导弹系统在计算时使用了double值,这种不精确性导致导弹无法跟踪和拦截来袭的伊拉克飞毛腿导弹。

(3)复制并粘贴之前编写的语句(使用了double变量)。

(4)修改语句,使用decimal并将变量重命名为c和d,如下所示:

	   decimal c=0.1M;
       decimal d=0.2M;
       if(c+d==0.3M)
       {
           Console.WriteLine($"{c}+{d} equal 0.3");
       }
       else
       {
           Console.WriteLine($"{c}+{d} does NOT equal 0.3");
       }

(5)运行控制台应用程序并查看结果,输出如下所示:

0.1+0.2 equals 0.3

decimal类型是精确的,因为这种类型可以将数字存储为大的整数并移动小数点。例如,可以将0.1存储为1,然后将小数点左移一位。再如,可以将12.75存储为1275,然后将小数点左移两位。

对整数使用int类型存储,而对不会与其他值比较的实数使用double类型进行存储。decimal类型适合用于货币、CAD绘图、一般工程学以及任何对实数的准确性要求较高的场合。

double类型有一些有用的特殊值:double.NaN表示不是数字,double.Epsilon是可以存储在double里的最小正数,double.Infinity意味着无限大的值。

2.3.4 存储布尔值

布尔值只能是如下两个字面值中的一个:true 或false。

2.3.5使用Visual Studio Code 工作区

在创建更多项目之前,下面先讨论一下工作区。

尽管可以继续为每个项目创建和打开单独的文件夹,但同时打开多个文件夹可能很有用。在Visual Studio中,名为工作区的特性可以实现这一点。

下面本章到目前为止创建的两个项目创建工作区。

(1)在Visual Studio Code 中导航到File|Save Workspace As...。

(2)输入Chapter02作为工作区的名称,更改到Chapter02文件夹,然后单击Save按钮

(3)导航到File|Add Folder to Workspace...。

(4)选择Basics文件夹,单击Add按钮,注意Basics和Numbers文件夹现在是Chapter02工作区的一部分。

在使用工作区时,在终端输入命令时要小心。在输入可能具有破坏性的命令之前,请确保处于正确的文件夹中!

2.3.6 存储任何类型的对象

有一种名为object的特殊类型,这种类型可以存储任何数据,但这种灵活性是以混乱的代码和可能较差的性能为代价的。由于这两个原因,你应该尽可能避免使用object类型。

(1)创建一个名为Variables的新文件夹,并将其添加到Chapter02工作区中。

(2)导航到菜单栏的Terminal(不是View下的Terminal)|New Terminal。

(3)选择Variables项目。

(4)输入如下用来创建新控制台应用程序的命令:dotnet new console。

(5)导航到View|Command Palette。

(6)输入并选择OmniSharp:Select Project。

(7)选择Variables项目,在Variables项目中打开Program.cs。

(8)在资源管理器中,在Variables 项目中打开Program.cs

(9)在Main方法中添加声明语句,并通过object类型来使用一些变量,如下所示:

 		    object height= 1.88;
            object name="Amir";
            Console.WriteLine($"{name}is {height} metres tall.");
            int length1=name.Length;
            int length2=((string)name).Length;
            Console.WriteLine($"{name} has {length2} characters.");
            Console.WriteLine("Hello World!");

(10)在终端输入dotnet run 执行代码,注意第四条语句不能编译,因为编译器不知道name变量的数据类型。

(11)将注释用的双斜杠加到想要注释掉的语句的开头。

(12)在终端输入dotnet run以执行代码。注意,如果程序员明确告诉编译器,object变量包含字符串,那么编译器可以访问字符串的长度,如下所示:

Amiris 1.88 metres tall.
Amir has 4 characters.

object类型自从C#的第一个版本就已经可用了,但是C#2.0及其后期版本有了更好的选择——泛型,参见6章,泛型可提供我们想要的灵活性,但没有性能开销。

2.3.7 动态存储类型

还有一种特殊类型名为dynamic,可用于存储任何类型的数据,并且灵活性相比object类型更强,代价是性能下降了。dynamic关键字是在C#4.0中引入的。但是,与object变量不同的是,存储在dynamic变量中的值可以在没用显示进行强制转换的情况下调用成员。

(1)在Main方法中添加如下语句,声明一个dynamic变量并为它赋予一个字符串值:

dynamic anotherName="Ahmed";

(2)添加如下语句以获得这个字符串的长度:

int length=anotherName.Length;

dynamic类型存在的限制是,Visual Studio Code 不能显示只能感知来帮助编写代码。这是因为编译器在编译期间不能检查类型是什么。相反,CLR会在运行期间检查成员,如果缺少成员,则抛出异常。

异常是指示出错的一种方式。第3章将详细介绍它们,并且说明如何处理它们。

2.3.8声明局部变量

局部变量是在方法中声明的,它们只在方法执行期间存在,一旦方法返回,分配给任何局部变量的内存都会被释放。

严格地说,值类型都会被释放,而引用类型必须等待垃圾收集。第6章将介绍值类型和引用类型之间的区别。

指定和推断局部变量的类型

下面进一步探讨使用特定类型声明的局部变量并使用类型推断。

(1)在Main方法中输入如下语句,使用特定的类型声明一些局部变量并赋值:

		int population=66_000_000;
        double weight=1.88;
        decimal price=4.99M;
        string fruit="Apples";
        char letter='Z';
        bool happy=true;

Visual Studio Code 将在每个变量名称的下方显示绿色波浪线,以警告这个变量虽然已经分配了,但却从未使用过它的值。

可以使用Var关键字来声明局部变量。编译器将从你在赋值操作符=之后赋予的值推断类型。

没用小数点的字面数字可推断为int类型,除非添加L后缀,这种情况下,则会推断为long类型。

带有小数点的字面数字可推断为double类型,除非添加M后缀(在这种情况下,可推断为decimal类型)或F后缀(在这种情况下,则推断为float类型)。双引号用来指示字符串变量,单引号用来知识char变量,true和false值则被推断为bool类型。

(2)修改前面的语句以使用var 关键字。

 		var population=66_000_000;
        var weight=1.88;
        var price=4.99M;
        var fruit="Apples";
        var letter='Z';
        var happy=true;

虽然使用var 关键字很方便,但有些开发人员却总是想避免使用它,以便读者更容易理解代码中使用的类型。就我个人而言,我只在类型明显的时候才使用var关键字。例如在下面的代码中,第一条语句与第二条语句都清楚地说明了变量的类型,但是第一条语句明显更短些。另外,第三条语句表述不清楚,所以第四条语句更好些。

		var xal1=new XmlDocument();
        XmlDocument xml2=new XmlDocument();
        var file=File.CreateText(@"C:\something.txt");
        StreamWriter file2=File.CreateText(@"c:\something.txt");

2.3.9 获取类型的默认值

除了string之外,大多数基本类型都是值类型,这意味着它们必须有值。可以使用default()操作符确定类型的默认值。

string类型是引用类型。这意味着string变量包含值的内存地址而不是值本身。引用类型的变量可以有控制,空值是字面值,便是变量尚未引用任何东西。空值是所有引用类型的默认值。

第6章将介绍更多关于值类型和引用类型的知识。

下面看默认值。

(1)在Main方法中添加如下语句以显示int、bool、DateTime和string类型的默认值:

default(int)=0
default(bool)=False
default(DateTime)=0001/1/1 0:00:00
default(string)=

2.3.10 存储多个值

当需要存储同一类型的多个值时,可以声明数组。例如,当需要在string数组中存储四个名称时,就可以这样做。

下面的代码可用来为存储四个字符串的数组分配内存。首先在索引位置0~3存储字符串值(数组时从0开始计数的,因此最后一项比数组长度小1).然后使用for语句循环遍历数组中的每一项,详见第3章。

(1)在Chapter02文件夹中创建一个名为Arrays的文件夹。

(2)将Arrays文件夹添加到Chapter02工作区。

(3)为Arrays项目创建一个新的终端窗口。

(4)在Arrays文件夹中创建一个新的控制台应用程序项目。

(5)选择Arrays作为OmniSharp的当前项目。

(6)在Arrays项目中,在Program.cs的Main方法中添加如下语句,以声明和使用字符串值数组:

 	    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]);
            } 

(7)运行结果为:

Kate
Jack
Rebecca
Tom

在分配内存的时候,数组的大小总是固定的,因此需要在实例化之前确定数组要存储多少项。

数组对于临时存储多个项很有用,但是在动态添加和删除项时,集合时更灵活的选择。现在不需要担心集合,第8章会讨论它们。

2.4处理空值

前面介绍了如何在变量中存储数字之类的基本值。但是,如果变量没有值呢?怎么表示呢?C#有空值的概念,空值可以用来指示变量没有赋值。

2.4.1 使值类型为null

默认情况下,像int和DateTime这样的值类型必须总是有值。但有时,例如,当读取存储在数据库中允许的空值或缺失值时,允许值类型为null是很方便的,我们称之为可空值类型。

通过在声明变量时将问号作为后缀添加到类型中来启用这一功能。下面来看一个例子。

(1)在Chapter02文件夹中创建一个名为NullHandling的新文件夹。

(2)将NullHandling 文件夹添加到Chapter02工作区。

(3)为NullHandling项目创建一个新的终端窗口。

(4)在NullHandling文件夹中创建一个新的控制台应用程序项目。

(5)在NullHandling作为OmniSharp的当前项目。

(6)在NullHandling项目中,在Program.cs的Main方法中添加如下语句,以声明int变量并赋值(包括null);

 	    int thisCannotBeNull=4;
            thisCannotBeNull=null; //compile error 
            int? thisCouldBeNull=null;
            Console.WriteLine(thisCouldBeNull);
            Console.WriteLine(thisCouldBeNull.GetValueOrDefault());
            
            thisCouldBeNull=7;
            Console.WriteLine(thisCouldBeNull);
            Console.WriteLine(thisCouldBeNull.GetValueOrDefault());

(7)注释掉出现编译错误的语句。

(8)运行控制台应用程序并查看结果,输出以下所示:

0
7
7

第一行是空的,因为输出的是空值!

理解可空引用类型

在许多编程语言中,空值的使用是如此普遍,以至于许多有经验的程序员从不怀疑空值的存在。但是在很多情况下,如果不允许变量有空值,就可以编写更好、更简单的代码。

C#8.0语言中最重要的变化是引入了可空引用类型和不可空引用类型。“但是等一下!”你可能会想,“引用类型以及可以为空了!”

你可能是对的,但是在C#8.0中,可以通过设置文件级或项目级选项来启用这一有用的新特性,从而将引用类型配置为不再允许空值。因此这对C#来说一个巨大的变化,所以微软决定选择加入这个特性。

这个新的C#语言特性需要几年的时间才能产生影响,因为有成千上万的现有库包和应用程序仍保持旧的行为。甚至微软也没有时间在所有的.NET核心包中完全实现这个新特性。在过渡期间,你可以为自己的项目选择如下几种方案。

  • 保持默认:不需要更改。不支持不可空引用类型。

  • opt-in project,opt-out files:在项目级别启用这一特性,对于需要与旧行为保持兼容和任何文件,选择退出。这是微软内部使用的方案,同时请更新自己的包以使用这个新特性。

  • Opt-in files:仅为单个文件启用这一特性。

    2.4.2启用可空引用类型和不可空引用类型

    要在项目级别启用这一特性,请将以下内容添加到项目文件中:

    <PropertyGroup>
        <Nullable>enable</Nullable>
    </PropertyGroup>
    

    要在文件级别禁用这一特性,请在代码文件的顶部添加以下内容:

    nullable disable

    要在文件级别启用这一特性,请在代码文件的顶部添加以下内容:

    nullable enable

    2.4.3声明不可为空的变量和参数

    如果启用了可空引用类型,并希望为引用类型分配空值,那么使用的语法必须与使值类型为null的相同:在类型声明后面添加?符号。

    那么,可空引用类型是如何工作的呢?下面看一个例子。在存储关于地址的信息时,可能希望强制存储街道、城市和地区信息,但建筑物信息可以留空(为null)。

    (1)在NullHadnling.csproj中添加一个元素来再用可空引用类型,如下所示:

    <Project Sdk="Microsoft.NET.Sdk">
        <PropertyGroup>
       	<OutputType>Exe</OutputType>
        <TargetFramework>.NETCoreapp3.0</TargetFramework>
        <Nullable>enable</Nullable>
    	</PropertyGroup>
    </Project>
    

    (2)在Program.cs文件的顶部添加如下语句以启用可空引用类型:

    nullable enable

    (3)在Program.cs中,在Program类上方的NullHandling 名称空间中添加如下语句,以声明包含四个字段的Address类:

     class Address
        {
            public string? Building;
            public string Street;
            public string City;
            public string Region;
        }
    

    (4)在Terminal输入dotnet run 观察输出结果。会看到很多警告。

    (5)将控制付出值分配给不可空的三个字段,如下所示:

    	public string Street=string.Empty;
            public string City=string.Empty;
            public string Region=string.Empty;
    

    (6)在Main方法中添加如下语句以实例化Address并设置其属性:

    	    var address=new Address();
                address.Building=null;
                address.Street=null;
                address.City="London";
                address.Region=null;
    

    (7)在Terminal输入dotnet run 观察警告内容。

    2.4.4 检查null

    检查可空引用类型变量或可空值类型变量当前是否包含空值非常重要,因为如果不包含空值,就可能会抛出异常NullReferenceException,从而导致错误。应该在使用可控变量之前检查空值,如下所示:

     	    string thisCouldBeNullStr=string.Empty;
                if(thisCouldBeNullStr!=null)
                {
                    int length=thisCouldBeNullStr.Length;
                }
    

    如果试图使用可能为空的变量的成员,请使用空条件运算符?.,如下所示:

     	    if(thisCouldBeNullStr!=null)
                {
                    int length=thisCouldBeNullStr.Length;
                }
    	//如果试图使用可能为空的变量的成员,请使用空条件运算符?.如下所示:
                string authorName=null;
                int x=authorName.Length;
                int? y=authorName?.Length;
    

    有时,你希望为结果分配一个变量或者使用另一个值,比如3(假设变量为null)。为此,可以使用空合并操作符??,如下所示:

     			var result =authorName?.Length ??3;
                Console.WriteLine(result);
    

    这章就写这么多啦。这也太长了,实在不行了。这一篇就写了我4个晚上,基础部分拆成2个把。太长了没法看啊。

    我创建了一个C#相关的交流群。用于分享学习资料和讨论问题。欢迎有兴趣的小伙伴:QQ群:542633085

posted @ 2021-04-25 23:41  杜文龙  阅读(779)  评论(1编辑  收藏  举报