赞
踩
原文:
annas-archive.org/md5/5ba4b421a6ba3d7c3a23406bab386ec0
译者:飞龙
C#是一种强大而多才多艺的面向对象编程(OOP)语言,可以打开各种职业道路。但是,与任何编程语言一样,学习 C#可能是具有挑战性的。由于有各种不同的资源可用,很难知道从哪里开始。
这就是The C# Workshop的用武之地。由行业专家撰写和审查,它提供了一个快节奏、支持性的学习体验,可以让您迅速编写 C#代码并构建应用程序。与其他侧重于干燥、技术性解释基础理论的软件开发书籍不同,这个研讨会剔除了噪音,使用引人入胜的例子来帮助您了解每个概念在现实世界中的应用。
在阅读本书时,您将解决模拟软件开发人员每天处理的问题的真实练习。这些小项目包括构建一个猜数字游戏,使用发布者-订阅者模型设计 Web 文件下载器,使用 Razor Pages 创建待办事项列表,使用 async/await 任务从斐波那契序列生成图像,以及开发一个温度单位转换应用程序,然后将其部署到生产服务器上。
通过本书,您将具备知识、技能和信心,可以推动您的职业发展,并应对 C#的雄心勃勃的项目。
本书适用于有志成为 C#开发人员的人。建议您在开始之前具备基本的核心编程概念知识。虽然不是绝对必要的,但有其他编程语言的经验会有所帮助。
Jason Hales自 2001 年 C#首次发布以来,一直在使用各种微软技术开发低延迟、实时应用程序。他是设计模式、面向对象原则和测试驱动实践的热心倡导者。当他不忙于编码时,他喜欢和妻子 Ann 以及他们在英国剑桥郡的三个女儿一起度过时间。
Almantas Karpavicius是一名领先的软件工程师,就职于信息技术公司 TransUnion。他已经是一名专业程序员超过五年。除了全职编程工作外,Almantas 在Twitch.tv上利用业余时间免费教授编程已经三年。他是 C#编程社区 C# Inn 的创始人,拥有 7000 多名成员,并创建了两个免费的 C#训练营,帮助数百人开始他们的职业生涯。他曾与编程名人进行采访,如 Jon Skeet、Robert C. Martin(Uncle Bob)、Mark Seemann,还曾是兼职的 Java 教师。Almantas 喜欢谈论软件设计、清晰的代码和架构。他还对敏捷(特别是 Scrum)感兴趣,是自动化测试的忠实粉丝,尤其是使用 BDD 进行的测试。他还拥有两年的微软 MVP 资格(packt.link/2qUJp
)。
Mateus Viegas在软件工程和架构领域工作了十多年,最近几年致力于领导和管理工作。他在技术上的主要兴趣是 C#、分布式系统和产品开发。他热爱户外活动,工作之余喜欢和家人一起探索大自然、拍照或者跑步。
第一章,你好,C#,介绍了语言的基本概念,如变量、常量、循环和算术和逻辑运算符。
第二章,构建高质量的面向对象代码,介绍了面向对象编程的基础知识和其四大支柱,然后介绍了清晰编码的五大主要原则——SOLID。本章还涵盖了 C#语言的最新特性。
第三章,委托、事件和 Lambda,介绍了委托和事件,它们构成了对象之间通信的核心机制,以及 lambda 语法,它提供了一种清晰表达代码意图的方式。
第四章,数据结构和 LINQ,涵盖了用于存储多个值的常见集合类,以及专为在内存中查询集合而设计的集成语言 LINQ。
第五章,并发:多线程并行和异步代码,介绍了编写高性能代码的基本知识,以及如何避免常见的陷阱和错误。
第六章,使用 SQL Server 的 Entity Framework,介绍了使用 SQL 和 C#进行数据库设计和存储,并深入研究了使用 Entity Framework 进行对象关系映射。本章还教授了与数据库一起工作的常见设计模式。
注
对于那些有兴趣学习数据库基础知识以及如何使用 PostgreSQL 的人,本书的 GitHub 存储库中包含了一个参考章节。你可以在以下链接访问:packt.link/oLQsL
。
第七章,使用 ASP.NET 创建现代 Web 应用程序,介绍了如何编写简单的 ASP.NET 应用程序,以及如何使用服务器端渲染和单页应用程序等方法来创建 Web 应用程序。
第八章,创建和使用 Web API 客户端,介绍了 API 并教你如何从 ASP.NET 代码访问和使用 Web API。
第九章,创建 API 服务,继续讨论 API 的主题,并教你如何为消费创建 API 服务,以及如何保护它。本章还向你介绍了微服务的概念。
注
此外,还有两个额外的章节(第十章,自动化测试,和第十一章,生产就绪的 C#:从开发到部署),你可以在以下链接找到:packt.link/44j2X
和 packt.link/39qQA
。
你也可以在packt.link/qclbF
的在线工作坊中找到所有活动的解决方案。
本书有一些约定用于高效安排内容。请在下一节中了解相关内容。
在书中,代码块设置如下:
using System;
namespace Exercise1_01
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
}
在输入和执行一些代码会立即输出的情况下,显示如下:
dotnet run
Hello World!
Good morning Mars!
定义、新术语和重要词汇显示如下:
多线程是一种并发形式,其中多个线程用于执行操作。
章节正文中的语言命令以以下方式表示:
在这里,最简单的Task
构造函数传递了一个Action
lambda 语句,这是实际要执行的目标代码。目标代码将消息Inside taskA
写入控制台。
基本信息以以下方式表示:
注
术语Factory
经常在软件开发中用来表示帮助创建对象的方法。
长代码片段被截断,相应的代码文件名称放在截断代码的顶部。整个代码的永久链接放在代码片段下方,如下所示:
HashSetExamples.cs
using System;
using System.Collections.Generic;
namespace Chapter04.Examples
{
}
You can find the complete code here: http://packt.link/ZdNbS.
在你深入学习 C#语言的强大之前,你需要安装.NET 运行时和 C#开发和调试工具。
你可以安装完整的 Visual Studio 集成开发环境(IDE),它提供了一个功能齐全的代码编辑器(这是一个昂贵的许可证),或者你可以安装 Visual Studio Code(VS Code),微软的轻量级跨平台编辑器。C#工作坊以 VS Code 编辑器为目标,因为它不需要许可证费用,并且可以在多个平台上无缝运行。
访问网址 https://code.visualstudio.com 并根据您选择的平台的安装说明在 Windows、macOS 或 Linux 上下载它。
注意
最好勾选“创建桌面图标”复选框以方便使用。
VS Code 是免费且开源的。它支持多种语言,需要为 C#语言进行配置。安装 VS Code 后,您需要添加C# for Visual Studio Code
(由 OmniSharp 提供支持)扩展以支持 C#。这可以在 https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp 找到。要安装 C#扩展,请按照每个平台的说明进行操作:
C#
。注意
如果您不想直接从网站安装 C#扩展,可以从 VS Code 本身安装。
选择第一个选择,即C# for Visual Studio Code (powered by OmniSharp)
。
点击“安装”按钮。
重新启动VS Code
:
图 0.1:安装 VS Code 的 C#扩展
您将看到 C#扩展成功安装在 VS Code 上。您现在已经在系统上安装了 VS Code。
下一节将介绍如何在您在书的章节之间移动时使用 VS Code。
要更改默认要构建的项目(无论是活动、练习还是演示),您需要指向这些练习文件:
tasks.json
/ tasks.args
launch.json
/ configurations.program
有两种不同的练习模式需要注意。一些练习有自己的项目。其他练习有不同的主方法。每个练习的单个项目的主方法可以按照以下方式进行配置(在此示例中,对于第三章,委托、事件和 Lambda,您正在配置练习 02为构建和启动点):
launch.json
{ "version": "0.2.0", "configurations": [ { "name": ".NET Core Launch (console)", "type": "coreclr", "request": "launch", "preLaunchTask": "build", "program": "${workspaceFolder}/Exercises/ /Exercise02/bin/Debug/net6.0/Exercise02.exe", "args": [], "cwd": "${workspaceFolder}", "stopAtEntry": false, "console": "internalConsole" } ] }
tasks.json
{ "version": "2.0.0", "tasks": [ { "label": "build", "command": "dotnet", "type": "process", "args": [ "build", "${workspaceFolder}/Chapter05.csproj", "/property:GenerateFullPaths=true", "/consoleloggerparameters:NoSummary" ], "problemMatcher": "$msCompile" }, ] }
每个练习(例如,“第五章 练习 02”)都可以按照以下方式进行配置:
launch.json
{ "version": "0.2.0", "configurations": [ { "name": ".NET Core Launch (console)", "type": "coreclr", "request": "launch", "preLaunchTask": "build", "program": "${workspaceFolder}/bin/Debug/net6.0/Chapter05.exe", "args": [], "cwd": "${workspaceFolder}", "stopAtEntry": false, "console": "internalConsole" } ] }
tasks.json
{ "version": "2.0.0", "tasks": [ { "label": "build", "command": "dotnet", "type": "process", "args": [ "build", "${workspaceFolder}/Chapter05.csproj", "/property:GenerateFullPaths=true", "/consoleloggerparameters:NoSummary", "-p:StartupObject=Chapter05.Exercises.Exercise02.Program", ], "problemMatcher": "$msCompile" }, ] }
现在您已经了解了launch.json
和tasks.json
,可以继续下一节,详细介绍.NET 开发平台的安装。
.NET 开发平台可以从dotnet.microsoft.com/download
下载。Windows、macOS 和 Linux 上都有不同的变体。C# Workshop书籍使用.NET 6.0。
按照以下步骤在 Windows 上安装.NET 6.0 平台:
Windows
平台选项卡:图 0.2:.NET 6.0 下载窗口
注意
图 0.2中显示的屏幕可能会根据 Microsoft 的最新发布而更改。
根据系统上安装的操作系统打开并完成安装。
安装后重新启动计算机。
按照以下步骤在 macOS 上安装.NET 6.0 平台:
选择macOS
平台选项卡(图 0.2)。
点击“下载.NET SDK x64”选项。
下载完成后,打开安装程序文件。您应该有一个类似图 0.3的屏幕:
图 0.3:macOS 安装开始屏幕
以下屏幕将确认安装所需的空间量:
[外链图片转存中…(img-H57zsth2-1720490526086)]
图 0.4:显示安装所需磁盘空间的窗口
您将在下一个屏幕上看到一个移动的进度条:
图 0.5:显示安装进度的窗口
安装完成后不久,您将看到一个成功的屏幕(图 0.6):
图 0.6:显示安装完成的窗口
dotnet –list-sdks
这将检查您计算机上安装的.NET 版本。图 0.7显示了您安装的 SDK 的列表:
图 0.7:在终端中检查安装的.NET SDK
通过这些步骤,您可以在计算机上安装.NET 6.0 SDK 并检查已安装的版本。
注意
Linux 的.NET 6.0 安装步骤未包括在内,因为它们与 Windows 和 macOS 相似。
在继续之前,了解.NET 6.0 的功能很重要。
.NET 6.0:这是 Windows 推荐的最新长期支持(LTS)版本。它可用于构建许多不同类型的应用程序。
.NET Framework 4.8:这是仅适用于 Windows 的版本,用于构建仅在 Windows 上运行的任何类型的应用程序。
.NET 图像:此开发平台可用于构建不同类型的应用程序。
.NET Core 图像:这为构建许多类型的应用程序提供了终身支持。
.NET 框架图像:这些是仅适用于 Windows 的.NET 版本,用于构建仅在 Windows 上运行的任何类型的应用程序。
在系统上安装了.NET 6.0 后,下一步是使用 CLI 配置项目。
安装了.NET 后,CLI 可用于创建和配置用于 VS Code 的项目。要启动.NET CLI,请在命令提示符下运行以下命令:
dotnet
如果.NET 安装正确,您将在屏幕上看到以下消息:
Usage: dotnet [options]
Usage: dotnet [path-to-application]
安装了 CLI 以配置 VS Code 项目后,您需要了解使用和扩展 SQL 语言的功能强大的开源对象关系数据库系统 PostgreSQL。
注意
您将首先按照 Windows 的说明安装 PostgreSQL,然后是 macOS,然后是 Linux。
在第六章中使用了 PostgreSQL,使用 SQL Server 的 Entity Framework。在继续进行该章之前,您必须按照以下步骤在系统上安装 PostgreSQL:
www.enterprisedb.com/downloads/postgres-postgresql-downloads
并下载 Windows 的最新版本安装程序:图 0.8:每个平台的最新 PostgreSQL 版本
注意
图 0.8中显示的屏幕可能会根据供应商的最新发布而更改。
图 0.9:用于上传 PostgreSQL 的欢迎屏幕
图 0.10:PostgreSQL 默认安装目录
保持默认的“安装目录”不变,然后单击“下一步”按钮。
从图 0.11的列表中选择以下内容:
PostgreSQL 服务器
指的是数据库。
pgAdmin 4
是数据库管理工具。
Stack Builder
是 PostgreSQL 环境构建器(可选)。
命令行工具
使用命令行与数据库一起工作。
图 0.11:选择要继续的 PostgreSQL 组件
然后点击“下一步”按钮。
在下一个屏幕上,“数据目录”屏幕要求您输入用于存储数据的目录。因此,输入数据目录名称:
图 0.12:存储数据的目录
一旦输入了数据目录,点击“下一步”按钮继续。下一个屏幕要求您输入密码。
输入新密码。
在“重新输入密码”旁边重新输入数据库超级用户的密码:
图 0.13:为数据库超级用户提供密码
然后点击“下一步”按钮继续。
下一个屏幕显示端口为5432
。使用默认端口,即5432
:
图 0.14:选择端口
点击“下一步”按钮。
“高级选项”屏幕要求您输入数据库集群的区域设置。将其保留为“[默认区域设置]”:
图 0.15:选择数据库集群的区域设置
然后点击“下一步”按钮。
当显示“预安装摘要”屏幕时,点击“下一步”按钮继续:
图 0.16:设置窗口显示准备安装消息
继续选择“下一步”按钮(保持默认设置不变),直到安装过程开始。
等待完成。完成后,将显示“完成 PostgreSQL 安装向导”屏幕。
取消选中“退出时启动堆栈生成器”选项:
图 0.17:安装完成,未选中堆栈生成器
堆栈生成器用于下载和安装其他工具。默认安装包含所有练习和活动所需的所有工具。
最后,点击“完成”按钮。
现在从 Windows 打开pgAdmin4
。
在“设置主密码”窗口中为连接到 PostgreSQL 中的任何数据库输入主密码:
图 0.18:设置连接到 PostgreSQL 服务器的主密码
注意
最好输入一个你能轻松记住的密码,因为它将用于管理所有其他凭据。
接下来点击“确定”按钮。
在 pgadmin 窗口的左侧,通过单击旁边的箭头展开“服务器”。
您将被要求输入您的 PostgreSQL 服务器密码。输入与步骤 22中输入的相同的密码。
出于安全原因,请勿点击“保存密码”:
图 0.19:为 PostgreSQL 服务器设置 postgres 用户密码
PostgreSQL 服务器密码是连接到 PostgreSQL 服务器并使用postgres
用户时使用的密码。
图 0.20:pgAdmin 4 仪表板窗口
要探索 pgAdmin 仪表板,请转到探索 pgAdmin 仪表板部分。
按照以下步骤在 macOS 上安装 PostgreSQL:
访问 Postgres 应用的官方网站,在 mac 平台上下载并安装 PostgreSQL:www.enterprisedb.com/downloads/postgres-postgresql-downloads
。
在 macOS 上下载最新的 PostgreSQL:
注意
以下屏幕截图是在 macOS Monterey(版本 12.2)上的版本 14.4 拍摄的。
图 0.21:PostgreSQL 的安装页面
图 0.22:启动 PostgreSQL 设置向导
图 0.23:选择安装目录
单击“下一步”按钮。
在下一个屏幕上,选择以下组件进行安装:
PostgreSQL 服务器
pgAdmin 4
命令行工具
Stack Builder
组件的选择:图 0.24:选择要安装的组件
选择完选项后,单击“下一步”按钮。
指定 PostgreSQL 将存储数据的数据目录:
图 0.25:指定数据目录
单击“下一步”按钮。
现在为 Postgres 数据库超级用户设置“密码”:
图 0.26:设置密码
确保安全地记下密码,以便登录到 PostgreSQL 数据库。
设置要运行 PostgreSQL 服务器的端口号。这里将默认端口号设置为5432
:
图 0.27:指定端口号
单击“下一步”按钮。
选择要由 PostgreSQL 使用的区域设置。在这里,“[默认区域设置]”是为 macOS 选择的区域设置:
图 0.28:选择区域设置
单击“下一步”按钮。
在下一个屏幕上,检查安装详细信息:
图 0.29:预安装摘要页面
最后,单击“下一步”按钮开始在您的系统上安装 PostgreSQL 数据库服务器的安装过程:
图 0.30:在开始安装过程之前准备安装页面
图 0.31:安装设置正在进行中
图 0.32:显示设置完成的成功消息
安装完成后,单击“完成”按钮。
现在在 PostgreSQL 服务器中加载数据库。
双击pgAdmin 4
图标,从启动台启动它。
输入在安装过程中设置的 PostgreSQL 用户的密码。
然后单击“确定”按钮。现在您将看到 pgAdmin 仪表板。
这样就完成了在 macOS 上安装 PostgreSQL。下一节将使您熟悉 PostgreSQL 界面。
在 Windows 和 macOS 上安装 PostgreSQL 后,按照以下步骤更好地了解界面:
从 Windows/macOS 打开pgAdmin4
(如果您的系统上没有打开 pgAdmin)。
在左侧单击“服务器”选项:
图 0.33:单击“服务器”以创建数据库
右键单击PostgreSQL 14
。
然后单击“创建”选项。
选择数据库…
选项以创建新数据库:
图 0.34:创建新数据库
这将打开一个创建-数据库窗口。
输入数据库名称,如TestDatabase
。
选择数据库的所有者或将其保留为默认值。现在,只需将Owner
设置为postgres
:
图 0.35:选择数据库的所有者
然后点击保存
按钮。这将创建一个数据库。
右键单击数据库
,然后选择刷新
按钮:
图 0.36:右键单击数据库后点击刷新按钮
现在在仪表板中显示了名为TestDatabase
的数据库:
图 0.37:TestDatabase 准备就绪
现在您的数据库已准备好在 Windows 和 Mac 环境中使用。
在此示例中,您正在使用 Ubuntu 20.04 进行安装。执行以下步骤:
要安装 PostgreSQL,请首先打开您的 Ubuntu 终端。
请确保使用以下命令更新您的存储库:
$ sudo apt update
$ sudo apt install postgresql postgresql-contrib
注意
要仅安装 PostgreSQL(不建议没有额外包),请使用命令$ sudo apt install postgresql
,然后按Enter
键。
此安装过程创建了一个名为postgres
的用户账户,该账户具有默认的Postgres
角色。
有两种方法可以使用postgres
用户账户启动 PostgreSQL CLI:
选项 1 如下:
$ sudo -i -u postgres
$ psql
注意
有时,在执行上述命令时,可能会显示psql
错误,如无法连接到服务器:没有这样的文件或目录
。这是由于系统上的端口问题。由于此端口阻塞,PostgreSQL 应用程序可能无法工作。您可以稍后再次尝试该命令。
$ \q
选项 2 如下:
$ sudo -u postgres psql
$ \q
conninfo
命令:$ sudo -u postgres psql
$ \conninfo
$ \q
使用此命令,您可以确保以端口5432
连接到postgres
数据库,作为postgres
用户。如果您不想使用默认用户postgres
,可以为自己创建一个新用户。
Enter
键创建一个新用户:$ sudo -u postgres createuser –interactive
上述命令将要求用户添加角色的名称及其类型。
输入角色的名称,例如testUser
。
然后,在提示时输入y
以设置新角色为超级用户:
Prompt:
Enter the name of the role to add: testUser
Shall the new role be a superuser? (y/n) y
这将创建一个名为testUser
的新用户。
testdb
的新数据库:$ sudo -u postgres createdb testdb
$ sudo -u testUser psql -d testdb
$ \conninfo
$ \q
使用此命令,您可以确保以端口5432
连接到testdb
数据库,作为testUser
用户。
通过这些步骤,您已经完成了 Ubuntu 上的 PostgreSQL 安装。
从 GitHub 下载代码packt.link/sezEm
。参考这些文件获取完整的代码。
本书中使用的高质量彩色图片可以在packt.link/5XYmX
找到。
如果您在安装过程中遇到任何问题或有任何问题,请发送电子邮件至workshops@packt.com
。
概述
本章介绍了 C#的基础知识。您将首先学习.NET 命令行界面(CLI)的基础知识以及如何使用 Visual Studio Code(VS Code)作为基本集成开发环境(IDE)。然后,您将了解各种 C#数据类型以及如何为这些类型声明变量,然后转到关于算术和逻辑运算符的部分。在本章结束时,您将知道如何处理异常和错误,并能够用 C#编写简单的程序。
C#是一种由微软团队在 2000 年代初创建的编程语言,由 Anders Hejlsberg 领导,他还是一些其他流行语言的创造者之一,如 Delphi 和 Turbo Pascal,这两种语言在上世纪 90 年代被广泛使用。在过去的 20 年中,C#已经发展和演变,如今它是全球范围内最广泛使用的编程语言之一,根据 Stack Overflow 的 2020 年洞察。
它有其在科技社区中占据如此崇高地位的原因。C#允许您为广泛的市场和设备编写应用程序。从具有高安全标准的银行业到拥有大量交易的电子商务公司,这是一种被需要性能和可靠性的公司信任的语言。此外,C#还可以编写 Web、桌面、移动甚至物联网应用程序,使您能够为几乎每种设备进行开发。
最初,C#只能在 Windows 上运行;然而,在过去几年中,C#团队已经做出了努力,使其跨平台兼容。如今,它可以与所有主要操作系统分发版一起使用,即 Windows、Linux 和 macOS。目标很简单:在任何地方开发、构建和运行 C#,让每个开发人员和团队选择他们最有效或最喜欢的环境。
C#的另一个显著特点是它是一种强类型的编程语言。您将在接下来的部分中更深入地了解这一点,并且您将看到强类型使得在编程时能够更好地保护数据安全。
此外,C#在过去几年已成为开源项目,由微软作为主要维护者。这是非常有利的,因为它允许语言从全球范围内不断获得改进,同时有一个坚实的支持公司来推广和投资。C#也是一种多范式语言,这意味着您可以以美观、简洁和适当的方式使用它来编写多种编程风格的软件。
在 C#世界中,您会经常听到一个术语,那就是.NET。它是 C#的基础,是该语言构建在其上的框架。它既有一个允许开发语言的软件开发工具包(SDK),也有一个允许语言运行的运行时。
话虽如此,要开始使用 C#进行开发,您只需要安装.NET SDK。此安装将在开发环境中提供编译器和运行时。在本节中,您将学习准备开发和在本地运行 C#的基本步骤。
注意
有关如何下载.NET 6.0 SDK 并在您的计算机上安装的逐步说明,请参阅本书的前言。
一旦完成了.NET 6.0 SDK 的安装,您将拥有一个称为.NET CLI 的东西。这个命令行界面(CLI)允许您使用非常简单的命令创建新项目、编译它们并直接从终端运行它们。
安装后,在您喜欢的终端上运行以下命令:
dotnet --list-sdks
您应该会看到以下输出:
6.0.100 [/usr/local/share/dotnet/sdk]
这个输出显示你的电脑上安装了 SDK 的 6.0.100 版本。这意味着你已经准备好开始开发你的应用程序了。如果你输入 dotnet -–help
,你会注意到几个命令会出现在 CLI 中供你选择运行。在这一部分,你将学习到最基本的命令,用来创建和运行应用程序:new
,build
和run
。
dotnet new
命令允许你创建一个引导项目来开始开发。CLI 有几个内置模板,它们只是各种类型应用程序的基本引导:web 应用程序,桌面应用程序等。在dotnet new
命令中,你必须指定两件事:
模板名称
项目名称
名称作为参数传递,这意味着你应该用-n
或–name
标志来指定它。命令如下:
dotnet new TYPE -n NAME
例如,要创建一个名为MyConsoleApp
的新控制台应用程序,你可以简单地输入:
dotnet new console -n MyConsoleApp
这将生成一个新的文件夹,其中包含一个名为MyConsoleApp.csproj
的文件,这是包含编译器构建项目所需的所有元数据的 C#项目文件,以及一些应用程序构建和运行所需的文件。
接下来,dotnet build
命令允许你构建一个应用程序并使其准备运行。这个命令应该只放在两个位置:
包含一个.csproj
文件的项目文件夹。
包含一个.sln
文件的文件夹。
解决方案(.sln
)文件是包含一个或多个项目文件的元数据文件。它们用于将多个项目文件组织成单个构建。
最后,第三个重要的命令是dotnet run
。这个命令允许你正确地运行一个应用程序。它可以在包含你的.NET 应用程序的.csproj
文件的文件夹中不带任何参数调用,或者在 CLI 上使用-–project
标志传递项目文件夹。run
命令还会在运行之前自动构建应用程序。
在阅读本书时,你将使用 Visual Studio Code (VS Code)作为你的代码编辑器。它适用于所有平台,你可以在 https://code.visualstudio.com/下载适合你操作系统的版本。虽然 VS Code 不是一个完整的集成开发环境(IDE),但它有很多扩展,使它成为一个强大的工具来开发和进行正确的 C#编码,无论使用的是哪个操作系统。
为了正确地开发 C#代码,你主要需要安装 Microsoft C#扩展。它使 VS Code 具备了代码补全和识别错误的能力,并且可以在marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp
上找到。
注意
在继续之前,建议你安装 VS Code 和 Microsoft C#扩展。你可以在本书的前言中找到安装过程的逐步说明。
为了运行,每个 C#程序都需要一个称为入口点的东西。在 C#中,程序的标准入口点是Main
方法。无论你的程序类型是什么,无论是 web 应用程序、桌面应用程序,甚至是一个简单的控制台应用程序,Main
方法都将是你的 C#程序的入口点。这意味着每次应用程序运行时,运行时都会在你的代码中搜索这个方法,并执行其中的代码块。
这个结构是由 CLI 用new
命令为你创建的。一个Program.cs
文件包含一个名为Program
的类,一个名为Main
的方法,这个方法又包含一个单一的指令,在程序构建和运行后将被执行。你以后会学到更多关于方法和类的知识,但现在只需要知道,一个类通常包含一组数据,并且可以通过这些方法对这些数据执行操作。
关于基本 C#概念的另一件重要的事情是//
。
在这个练习中,您将看到在上一节学习的 CLI 命令,因为您将构建您的第一个 C#程序。这将是一个简单的控制台应用程序,将在控制台上打印Hello World
。
执行以下步骤:
dotnet new console -n Exercise1_01
这个命令将在Exercise1_01
文件夹中创建一个新的控制台应用程序。
dotnet run --project Exercise1_01
您应该看到以下输出:
图 1.1:“Hello World”在控制台上的输出
注意
您可以在packt.link/HErU6
找到本练习使用的代码。
在这个练习中,您创建了可能是最基本的 C#程序,一个控制台应用程序,将一些文本打印到提示符上。您还学会了如何使用.NET CLI,这是内置在.NET SDK 中用于创建和管理.NET 项目的机制。
现在继续下一节,了解如何编写顶级语句。
您可能已经注意到,在练习 1.01中,默认情况下,当您创建一个控制台应用程序时,会有一个包含以下内容的Program.cs
文件:
一个名为Program
的类。
静态 void Main
关键字。
您将在以后详细了解类和方法,但现在,为了简单起见,您不需要这些资源来创建和执行 C#程序。最新版本(.NET 6)引入了一个功能,使编写简单程序变得更加容易和简洁。例如,考虑以下内容:
using System;
namespace Exercise1_01
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
}
您可以用以下两行代码简单地替换这段代码:
using System;
Console.WriteLine("Hello World!");
通过使用这样的顶级语句,您可以编写简洁的程序。您可以简单地将要执行的语句放在程序的顶部。这对于加快学习 C#的速度也很有用,因为您不需要担心提前学习高级概念。唯一需要注意的是,项目只能有一个包含顶级语句的文件。
这就是为什么在本章中,您会发现所有练习都使用这种格式,以尽可能清晰地表达事物。
现在,您将迈出创建自己程序的第一步。本节将深入探讨变量的概念——它们是什么以及如何使用它们。
变量是给计算机内存位置的名称,用于保存可能变化的一些数据。要使变量存在,首先必须用类型和名称声明它。它也可以有一个赋给它的值。变量的声明可以通过几种不同的方式实现。
关于 C#中变量命名约定的一些基本考虑:
名称必须是唯一的,以字母开头,只能包含字母、数字和下划线字符(_
)。名称也可以以下划线字符开头。
名称是区分大小写的;因此,myVariable
和MyVariable
是不同的名称。
保留关键字,如int
或string
,不能作为名称使用(这是编译器的限制),除非在名称前加上@
符号,如@int
或@string
。
变量可以以显式和隐式两种方式声明。声明的两种风格各有利弊,您将在下一节中探讨。
变量可以通过同时写出其类型和值来显式声明。假设您想创建两个变量,a
和b
,都包含整数。显式声明如下所示:
int a = 0;
int b = 0;
在使用变量之前,必须为变量赋值。否则,C#编译器在构建程序时会报错。以下示例说明了这一点:
int a;
int b = a; // The compiler will prompt an error on this line: Use of unassigned local variable
在同一行中声明多个变量也是可能的,就像在以下代码片段中一样,您在声明三个变量;两个保存值100
,一个保存值10
:
int a, b = 100, c = 10;
请记住,C#是一种强类型的编程语言;这意味着变量总是与一个类型相关联。无论类型是隐式声明还是显式声明,都无关紧要。使用var
关键字,C#编译器将根据分配给它的值推断变量类型。
考虑你想要创建一个变量,使用这种方法来保存一些文本。可以通过以下语句来实现:
var name = "Elon Musk";
要将文本存储在变量中,你应该用双引号("
)开始和结束文本。在上面的例子中,通过查看被赋给name
的值,C#知道这个变量所持有的类型是字符串,即使在语句中没有提到类型。
显式声明增强了类型声明的可读性,这是这种技术的主要优势之一。另一方面,它们往往会让代码变得更冗长,特别是在使用一些数据类型(稍后会看到)时,比如Collections
。
基本上,决定声明的风格取决于程序员的个人偏好,并且在某些情况下可能会受到公司指南的影响。在学习的过程中,建议你选择一种使你的学习路径更加顺畅的方式,因为从纯技术角度来看,几乎没有实质性的差异。
在下一个练习中,你将通过为来自用户与控制台应用程序的交互的输入分配变量来自己完成这个任务,用户将被要求输入他们的名字。要完成这个练习,你将使用 C#提供的以下内置方法,这些方法在你的 C#之旅中经常会用到:
Console.ReadLine()
: 这允许你检索用户在控制台上提示的值。
Console.WriteLine()
: 这将传递作为参数的值作为输出写入到控制台。
在这个练习中,你将创建一个交互式的控制台应用程序。该应用程序应该询问你的名字,一旦提供,它应该显示一个带有你的名字的问候语。
要完成这个练习,请执行以下步骤:
dotnet new console -n Exercise1_02
这个命令在Exercise1_02
文件夹中创建一个新的控制台应用程序。
Program.cs
文件。将以下内容粘贴到Main
方法中:Console.WriteLine("Hi! I'm your first Program. What is your name?");
var name = Console.ReadLine();
Console.WriteLine($"Hi {name}, it is very nice to meet you. We have a really fun journey ahead.");
dotnet run --project Exercise1_02
这将输出以下内容:
Hi! I'm your first Program. What is your name?
Enter
。例如,如果你输入Mateus
,输出将会是:Hi! I'm your first Program. What is your name?
Mateus
Hi Mateus, it is very nice to meet you. We have a really fun journey ahead.
注意
你可以在packt.link/1fbVH
找到用于这个练习的代码。
你已经更熟悉变量是什么,如何声明它们,以及如何给它们赋值。现在是时候开始讨论这些变量可以存储什么数据,更具体地说,有哪些数据类型。
在这一部分,你将讨论 C#中的主要数据类型及其功能。
C#使用string
关键字来标识存储文本的数据,作为字符序列。你可以以几种方式声明字符串,如下面的代码片段所示。然而,当将一些值赋给字符串变量时,你必须将内容放在一对双引号之间,就像在最后两个例子中看到的那样:
// Declare without initializing.
string message1;
// Initialize to null.
string message2 = null;
// Initialize as an empty string
string message3 = System.String.Empty;
// Will have the same content as the above one
string message4 = "";
// With implicit declaration
var message4 = "A random message" ;
一种简单但有效的技术(你在前面的练习 1.02中使用过)叫做字符串插值。通过这种技术,很容易将纯文本值与变量值混合在一起,使文本在这两者之间组合。你可以通过以下步骤来组合两个或更多个字符串:
在初始引号之前,插入一个$
符号。
现在,在字符串内部,放置花括号和你想要放入字符串中的变量的名称。在这种情况下,通过在初始字符串中放置{name}
来实现:
$"Hi {name}, it is very nice to meet you. We have a really fun journey ahead.");
关于字符串的另一个重要事实是它们是不可变的。这意味着字符串对象在创建后无法更改。这是因为 C#中的字符串是字符数组。数组是一种数据结构,它们收集相同类型的对象并具有固定的长度。您将在接下来的部分详细介绍数组。
在下一个练习中,您将探索字符串的不可变性。
在这个练习中,您将使用两个字符串来演示字符串引用始终是不可变的。执行以下步骤:
dotnet new console -n Exercise1_03
Program.cs
文件,并创建一个返回类型为void
的方法,用于替换字符串的一部分,如下所示:static void FormatString(string stringToFormat)
{
stringToFormat.Replace("World", "Mars");
}
在上面的代码片段中,使用Replace
函数将第一个字符串(在本例中为World
)替换为第二个字符串(Mars
)。
static string FormatReturningString(string stringToFormat)
{
return stringToFormat.Replace("Earth", "Mars");
}
var greetings = "Hello World!";
FormatString(greetings);
Console.WriteLine(greetings);
var anotherGreetings = "Good morning Earth!";
Console.WriteLine(FormatReturningString(anotherGreetings));
dotnet run --project Exercise1_03
。您应该在控制台上看到以下输出:dotnet run
Hello World!
Good morning Mars!
注意
您可以在packt.link/ZoNiw
找到此练习使用的代码。
通过这个练习,您看到了字符串不可变性的概念。当您传递一个作为方法参数的引用类型字符串(Hello World!
)时,它不会被修改。这就是当您使用返回void
的FormatString
方法时发生的情况。由于字符串不可变性,将创建一个新字符串,但不会分配给任何变量,原始字符串保持不变。而第二个方法返回一个新字符串,然后将该字符串打印到控制台。
尽管字符串是引用值,但当您使用.Equals()
方法、相等运算符(==
)和其他运算符(如!=
)时,实际上是在比较字符串的值,如下例所示:
string first = "Hello.";
string second = first;
first = null;
现在,您可以比较这些值,并调用Console.WriteLine()
输出结果,如下所示:
Console.WriteLine(first == second);
Console.WriteLine(string.Equals(first, second));
运行上述代码将产生以下输出:
False
False
您会得到这个输出,因为尽管字符串是引用类型,但==
和.Equals
比较都是针对字符串值的。还要记住字符串是不可变的。这意味着当您将second
赋给first
并将first
设置为null
时,将为first
创建一个新值,因此second
的引用不会改变。
C#将其数字类型细分为两大类——整数和浮点类型数字。整数类型数字如下:
sbyte
:保存从-128 到 127 的值
short
:保存从-32,768 到 32,767 的值
int
:保存从-2,147,483,648 到 2,147,483,647 的值
long
:保存从-9,223,372,036,854,775,808 到 9,223,372,036,854,775,807 的值
决定使用哪种整数类型取决于您要存储的值的大小。
所有这些类型都被称为有符号值。这意味着它们可以存储负数和正数。还有另一系列称为无符号类型的类型。无符号类型包括byte
、ushort
、uint
和ulong
。它们之间的主要区别在于有符号类型可以存储负数,而无符号类型只能存储大于或等于零的数字。您将大部分时间使用有符号类型,所以不用担心一次记住所有这些。
另一类别,即浮点类型,指的是用于存储一个或多个小数点的数字的类型。C#中有三种浮点类型:
float
:占用 4 字节,可以存储从± 1.5 x 10−45 到± 3.4 x 1038 的数字,精度范围为 6 到 9 位。要使用var
声明一个浮点数,您可以简单地在数字后面添加f
,如下所示:var myFloat = 10f;
double
:占用 8 字节,可以存储从± 5.0 × 10−324 到± 1.7 × 1030 的数字,精度范围为 15 到 17 位。要使用 var 声明一个双精度数,您可以在数字后面添加 d,如下所示:var myDouble = 10d;
decimal
:占用 16 字节,可以存储从± 1.0 x 10-28 到± 7.9228 x 1028 的数字,精度范围为 28 到 29 位。要使用 var 声明一个十进制数,您只需在数字后面添加 m,如下所示:var myDecimal = 10m;
选择浮点类型主要取决于所需的精度程度。例如,decimal
主要用于需要非常高精度且不能依赖四舍五入进行精确计算的金融应用。对于 GPS 坐标,如果需要处理通常具有 10 位数字的亚米精度,double
变量可能是合适的选择。
在选择数字类型时要考虑的另一个相关点是性能。分配给变量的内存空间越大,对这些变量的操作性能就越低。因此,如果不需要高精度,float
变量的性能将优于double
,而double
的性能又将优于decimal
。
在这里,您了解了变量及其主要类型。现在,您将使用它们执行一些基本的计算,如加法、减法和乘法。这可以通过 C#中可用的算术运算符来实现,如+
、-
、/
和*
。因此,继续进行下一个练习,您将使用这些运算符创建一个基本的计算器。
在这个练习中,您将创建一个简单的计算器,接收两个输入,并根据所选的算术运算显示它们之间的结果。
以下步骤将帮助您完成此练习:
dotnet new console -n Exercise1_04
Program.cs
文件,在Main
方法中声明两个变量,读取用户输入,如下所示:Console.WriteLine("Type a value for a: ");
var a = int.Parse(Console.ReadLine());
Console.WriteLine("Now type a value for b: ");
var b = int.Parse(Console.ReadLine());
前面的代码片段使用.ReadLine
方法读取输入。但是,这个方法返回一个string
,而您需要评估一个数字。因此,在这里使用了Parse
方法。所有数字类型都有一个名为 Parse 的方法,它接收一个字符串并将其转换为数字。
Main
方法中:Console.WriteLine($"The value for a is { a } and for b is { b }");
Console.WriteLine($"Sum: { a + b}");
Console.WriteLine($"Multiplication: { a * b}");
Console.WriteLine($"Subtraction: { a - b}");
Console.WriteLine($"Division: { a / b}");
dotnet run
命令运行程序,如果您输入10
和20
,您应该会看到以下输出:Type a value for a:
10
Now type a value for b:
20
The value for a is 10 and b is 20
Sum: 30
Multiplication: 200
Subtraction: -10
Division: 0
注意
您可以在packt.link/ldWVv
找到此练习中使用的代码。
因此,您已经使用算术运算符在 C#中构建了一个简单的计算器应用程序。您还了解了解析的概念,用于将字符串转换为数字。在下一节中,您将简要介绍类的主题,这是 C#编程的核心概念之一。
类是 C#编码的一个重要部分,并将在第二章,构建高质量面向对象的代码中得到全面覆盖。本节简要介绍了类的基础知识,以便您可以开始在程序中使用它们。
在 C#中,保留的class
关键字用于定义对象的类型。对象,也可以称为实例,实际上就是分配了存储信息的内存块。根据这个定义,类的作用是作为对象的蓝图,具有一些属性来描述这个对象,并通过方法指定这个对象可以执行的操作。
例如,假设你有一个名为Person
的类,有两个属性Name
和Age
,以及一个检查Person
是否为孩子的方法。方法是可以放置逻辑以执行某些操作的地方。它们可以返回特定类型的值,也可以有特殊的void
关键字,表示它们不返回任何东西,只是执行某些操作。你也可以有方法调用其他方法:
public class Person { public Person() { } public Person(string name, int age) { Name = name; Age = age; } public string Name { get; set; } public int Age { get; set; } public void GetInfo() { Console.WriteLine($"Name: {Name} – IsChild? {IsChild()}"); } public bool IsChild() { return Age < 12; } }
然而,还有一个问题。由于类充当蓝图(或者如果你喜欢的话,是定义),你如何实际分配内存来存储类定义的信息?这是通过一个称为实例化的过程完成的。当你实例化一个对象时,你在内存中为它分配一些空间,在一个称为堆的保留区域中。当你将一个变量分配给一个对象时,你正在设置该变量具有这个内存空间的地址,这样每次你操作这个变量时,它指向并操作分配在这个内存空间的数据。以下是一个实例化的简单示例:
var person = new Person();
请注意,Person
有两个魔术关键字get
和set
的属性。Getter 定义了可以检索属性值,setter 定义了可以设置属性值。
这里还有一个重要的概念是构造函数。构造函数是一个没有返回类型的方法,通常出现在类的顶层,以提高可读性。它指定了创建对象所需的内容。默认情况下,类将始终具有一个无参数的构造函数。如果定义了带参数的另一个构造函数,类将被限制为只有这一个。在这种情况下,如果你仍然想要一个无参数的构造函数,你必须指定一个。这是非常有用的,因为类可以有多个构造函数。
也就是说,你可以通过以下方式为具有 setter 的对象属性分配值:
var person = new Person("John", 10);
var person = new Person() { Name = "John", Age = 10 };
var person = new Person();
person.Name = "John";
person.Age = 10;
类还有很多你将在后面看到的内容。现在,主要思想如下:
类是对象的蓝图,可以具有描述这些对象的属性和方法。
对象需要被实例化,这样你才能对它们进行操作。
类默认有一个无参数的构造函数,但可以根据需要有许多自定义的构造函数。
对象变量是包含对象在专用内存部分中分配的特殊内存空间的内存地址的引用。
在 C#中,可以使用DateTime
值类型来表示日期。它是一个具有两个静态属性的结构,称为MinValue
,即公元 0001 年 1 月 1 日 00:00:00,和MaxValue
,即公元 9999 年 12 月 31 日 23:59:59。正如名称所示,这两个值代表了根据公历日期格式的最小和最大日期。DateTime
对象的默认值是MinValue
。
可以以各种方式构造DateTime
变量。其中一些最常见的方式如下:
var now = DateTime.Now;
这将变量设置为调用计算机上的当前日期和时间,表示为本地时间。
var now = DateTime.UtcNow;
这将变量设置为协调世界时(UTC)表示的当前日期和时间。
你还可以使用构造函数来传递天、月、年、小时、分钟,甚至秒和毫秒。
DateTime
对象还有一个特殊属性叫做Ticks
。它是自DateTime.MinValue
以来经过的 100 纳秒的数量。每次你有这种类型的对象,你都可以调用Ticks
属性来获得这样的值。
日期的另一种特殊类型是TimeSpan
结构。TimeSpan
对象表示以天、小时、分钟和秒为单位的时间间隔。在获取日期之间的间隔时很有用。现在你将看到这在实践中是什么样子的。
在此练习中,您将使用TimeSpan
方法/结构来计算本地时间和 UTC 时间之间的差异。要完成此练习,请执行以下步骤:
dotnet new console -n Exercise1_05
打开Program.cs
文件。
将以下内容粘贴到Main
方法中并保存文件:
Console.WriteLine("Are the local and utc dates equal? {0}", DateTime.Now.Date == DateTime.UtcNow.Date);
Console.WriteLine("\nIf the dates are equal, does it mean that there's no TimeSpan interval between them? {0}",
(DateTime.Now.Date - DateTime.UtcNow.Date) == TimeSpan.Zero);
DateTime localTime = DateTime.Now;
DateTime utcTime = DateTime.UtcNow;
TimeSpan interval = (localTime - utcTime);
Console.WriteLine("\nDifference between the {0} Time and {1} Time: {2}:{3} hours",
localTime.Kind.ToString(),
utcTime.Kind.ToString(),
interval.Hours,
interval.Minutes);
Console.Write("\nIf we jump two days to the future on {0} we'll be on {1}",
new DateTime(2020, 12, 31).ToShortDateString(),
new DateTime(2020, 12, 31).AddDays(2).ToShortDateString());
在前面的代码片段中,您首先检查了当前本地日期和 UTC 日期是否相等。然后,您使用TimeSpan
方法检查它们之间的间隔(如果有的话)。接下来,它打印了本地和 UTC 时间之间的差异,并打印了比当前日期提前两天的日期(在本例中为31/12/2020
)。
dotnet run --project Exercise1_05
您应该看到类似以下的输出:
Are the local and utc dates equal? True
If the dates are equal, does it mean there's no TimeSpan interval between them? True
Difference between the Local Time and Utc Time: 0:0 hours
If we jump two days to the future on 31/12/2020 we'll be on 02/01/2021
注意
您可以在packt.link/WIScZ
找到用于此练习的代码。
请注意,根据您所在的时区,您可能会看到不同的输出。
还可以将DateTime
值格式化为本地化字符串。这意味着根据 C#语言中称为文化的特殊概念格式化DateTime
实例,文化是您本地时间的表示。例如,不同国家的日期表示方式不同。现在看一下以下示例,在这些示例中,日期以法国和美国使用的格式输出:
var frenchDate = new DateTime(2008, 3, 1, 7, 0, 0);
Console.WriteLine(frenchDate.ToString(System.Globalization.CultureInfo.
CreateSpecificCulture("fr-FR")));
// Displays 01/03/2008 07:00:00
var usDate = new DateTime(2008, 3, 1, 7, 0, 0);
Console.WriteLine(frenchDate.ToString(System.Globalization.CultureInfo.CreateSpecificCulture("en-US")));
// For en-US culture, displays 3/1/2008 7:00:00 AM
还可以明确定义您希望日期输出的格式,就像以下示例中一样,您传递yyyyMMddTHH:mm:ss
值以表示您希望日期按年、月、日、小时、以冒号开头的分钟,最后是以冒号开头的秒输出:
var date1 = new DateTime(2008, 3, 1, 7, 0, 0);
Console.WriteLine(date1.ToString("yyyyMMddTHH:mm:ss"));
将显示以下输出:
20080301T07:00:00
您已经熟悉这些。回想一下,在前面的练习中,您进行了以下比较:
var now = DateTime.Now.Date == DateTime.UtcNow.Date;
此输出将在日期相等时将值true
分配给now
。但是如您所知,它们可能不一定相同。因此,如果日期不同,将分配一个false
值。这两个值是这样的布尔表达式的结果,并称为布尔值。这就是为什么now
变量的类型是bool
的原因。
布尔表达式是每个程序中每个逻辑比较的基础。基于这些比较,计算机可以在程序中执行某种行为。以下是一些布尔表达式和变量赋值的其他示例:
a
是否大于b
的比较结果:var basicComparison = a > b;
b
是否大于或等于a
的比较结果:bool anotherBasicComparison = b >= a;
var animal1 = "Leopard";
var animal2 = "Lion";
bool areTheseAnimalsSame = animal1 == animal2;
显然,先前比较的结果将是false
,并且此值将分配给areTheseAnimalsSame
变量。
现在您已经了解了布尔变量和它们的工作原理,是时候看一些逻辑运算符,您可以使用这些运算符来比较布尔变量和表达式了。
&&
(AND)运算符:此运算符将执行相等比较。如果两者相等,则返回true
,如果它们不相等,则返回false
。考虑以下示例,在这个示例中,您检查两个字符串的长度是否为0
:bool areTheseStringsWithZeroLength = "".Length == 0 && " ".Length == 0;
Console.WriteLine(areTheseStringsWithZeroLength);// will return false
||
(OR)运算符:此运算符将检查要比较的值中是否有一个为true
。例如,在这里,您正在检查至少一个字符串的长度是否为零:bool isOneOfTheseStringsWithZeroLength = "".Length == 0 || " ".Length == 0;
Console.WriteLine(isOneOfTheseStringsWithZeroLength); // will return true
!
(NOT)运算符:此运算符获取布尔表达式或值并对其取反;也就是说,它返回相反的值。例如,考虑以下示例,在这个示例中,您对检查一个字符串是否为零长度的比较结果取反:bool isOneOfTheseStringsWithZeroLength = "".Length == 0 || " ".Length == 0;
bool areYouReallySure = !isOneOfTheseStringsWithZeroLength;
Console.WriteLine(areYouReallySure); // will return false
到目前为止,你已经学习了类型、变量和运算符。现在是时候进入帮助你在现实世界问题中使用这些概念的机制了,也就是决策语句。
在 C#中,if-else
语句是实现代码分支的最受欢迎的选择之一,这意味着告诉代码在满足条件时遵循一条路径,否则遵循另一条路径。它们是逻辑语句,根据布尔表达式的评估结果继续程序的执行。
例如,你可以使用if-else
语句来检查密码是否满足某些条件(比如至少有六个字符和一个数字)。在下一个练习中,你将在一个简单的控制台应用程序中做到这一点。
在这个练习中,你将使用if-else
语句编写一个简单的凭据检查程序。应用程序应该要求用户输入他们的用户名;除非这个值至少有六个字符的长度,否则用户无法继续。一旦满足这个条件,用户应该被要求输入一个密码。密码也应该至少有六个字符,包含至少一个数字。只有在满足这两个条件之后,程序才应该显示一个成功消息,比如User successfully registered
。
以下步骤将帮助你完成这个练习:
Exercise1_06
的新控制台项目:dotnet new console -n Exercise1_06
Main
方法中,添加以下代码来询问用户用户名,并将值赋给一个变量:Console.WriteLine("Please type a username. It must have at least 6 characters: ");
var username = Console.ReadLine();
if (username.Length < 6)
{
Console.WriteLine($"The username {username} is not valid.");
}
else
子句中,你将继续验证并要求用户输入一个密码。一旦用户输入了密码,需要检查三个点。第一个条件是检查密码是否至少有六个字符,然后检查是否至少有一个数字。然后,如果这些条件中的任何一个失败,控制台应该显示一个错误消息;否则,它应该显示一个成功消息。添加以下代码来实现这一点:else { Console.WriteLine("Now type a password. It must have a length of at least 6 characters and also contain a number."); var password = Console.ReadLine(); if (password.Length < 6) { Console.WriteLine("The password must have at least 6 characters."); } else if (!password.Any(c => char.IsDigit©)) { Console.WriteLine("The password must contain at least one number."); } else { Console.WriteLine("User successfully registered."); } }
从上面的片段中,你可以看到如果用户输入少于六个字符,就会显示一个错误消息The password must have at least 6 characters.
。如果密码不包含一个数字但满足前面的条件,就会显示另一个错误消息The password must contain at least one number.
。
注意这里使用的逻辑条件是!password.Any(c => char.IsDi©(c))
。你将在第二章,构建高质量面向对象的代码中学到更多关于=>
符号的知识,但现在你只需要知道这行代码检查密码中的每个字符,并使用IsDigit
函数来检查字符是否是数字。这对每个字符都做了,如果没有找到数字,就显示错误消息。如果所有条件都满足,就显示成功消息User successfully registered.
。
dotnet run
运行程序。你应该会看到如下输出:Please type a username. It must have at least 6 characters:
thekingjames
Now type a password. It must have at least 6 characters and a number.
James123!"#
User successfully registered
注意
你可以在packt.link/3Q7oK
找到本练习使用的代码。
在这个练习中,你使用了 if-else 分支语句来实现一个简单的用户注册程序。
另一个简单易用但有效的决策运算符是三元运算符。它允许你根据布尔比较来设置变量的值。例如,考虑以下例子:
var gift = person.IsChild() ? "Toy" : "Clothes";
在这里,您使用?
符号来检查它之前放置的布尔条件是否有效。编译器为person
对象运行IsChild
函数。如果该方法返回true
,则将第一个值(:
符号之前)分配给gift
变量。如果该方法返回false
,则将第二个值(:
符号之后)分配给gift
变量。
三元运算符简单明了,可以根据简单的布尔验证进行赋值。在 C#的学习过程中,您会经常使用它。
C#中有两种类型的变量,即引用类型和值类型。值类型的变量,如结构体,包含值本身,正如其名称所示。这些值存储在称为堆栈的内存空间中。当声明此类类型的变量时,会分配特定的内存空间来存储该值,如下图所示:
图 1.2:值类型变量的内存分配
在这里,变量的值,即5
,存储在 RAM 中的位置0x100
的内存中。C#的内置值类型包括bool
、byte
、char
、decimal
、double
、enum
、float
、int
、long
、sbyte
、short
、struct
、uint
、ulong
和ushort
。
然而,引用类型变量的情况是不同的。在本章中,您需要了解的三种主要引用类型是string
、数组和class
。当分配新的引用类型变量时,存储在内存中的不是值本身,而是值被分配的内存地址。例如,考虑以下图表:
图 1.3:引用类型变量的内存分配
在这里,内存中存储的是字符串变量(Hello
)的地址,而不是其值。为简洁起见,您不会深入探讨这个话题,但重要的是要知道以下几点:
当值类型变量作为参数传递或分配为另一个变量的值时,.NET 运行时会将变量的值复制到另一个对象。这意味着原始变量不会受到在新的和后续变量中所做的任何更改的影响,因为这些值实际上是从一个地方复制到另一个地方的。
当引用类型变量作为参数传递或分配为另一个变量的值时,.NET 传递的是堆内存地址,而不是值。这意味着在方法内部对该变量进行的每次更改都会在外部反映出来。
例如,考虑以下处理整数的代码。在这里,您声明一个名为a
的int
变量,并将值100
赋给它。稍后,您创建另一个名为b
的int
变量,并将a
的值赋给它。最后,您修改b
,使其增加100
:
using System;
int a = 100;
Console.WriteLine($"Original value of a: {a}");
int b = a;
Console.WriteLine($"Original value of b: {b}");
b = b + 100;
Console.WriteLine($"Value of a after modifying b: {a}");
Console.WriteLine($"Value of b after modifying b: {b}");
a
和b
的值将显示在以下输出中:
Original value of a: 100
Original value of b: 100
Value of a after modifying b: 100
Value of b after modifying b: 200
在这个例子中,从a
中复制的值被复制到了b
中。从这一点开始,您对b
所做的任何其他修改都只会反映在b
中,而a
将继续保持其原始值。
那么,如果您将引用类型作为方法参数传递会怎样呢?考虑以下程序。在这里,您有一个名为Car
的类,具有两个属性—Name
和GearType
。程序内部有一个名为UpgradeGearType
的方法,该方法接收Car
类型的对象并将其GearType
更改为Automatic
:
using System; var car = new Car(); car.Name = "Super Brand New Car"; car.GearType = "Manual"; Console.WriteLine($"This is your current configuration for the car {car.Name}: Gea–Type - {car.GearType}"); UpgradeGearType(car); Console.WriteLine($"You have upgraded your car {car.Name} for the GearType {car.GearType}"); void UpgradeGearType(Car car) { car.GearType = "Automatic"; } class Car { public string Name { get; set; } public string GearType { get; set; } }
创建Car
UpgradeGearType()
方法后,输出将如下所示:
This is your current configuration for the car Super Brand New Car: GearType – Manual
You have upgraded your car Super Brand New Car for the GearType Automatic
因此,您会发现,如果您将一个car
(在这种情况下)作为参数传递给一个方法(在本例中为UpgradeGearType
),则在方法调用之后,内部和外部都会反映出对对象所做的任何更改。这是因为引用类型指的是内存中的特定位置。
在这个练习中,你将看到值类型和引用类型的相等性比较是不同的。执行以下步骤来完成:
dotnet new console -n Exercise1_07
Program.cs
文件。在同一个文件中,创建一个名为GoldenRetriever
的结构,具有一个Name
属性,如下所示:struct GoldenRetriever
{
public string Name { get; set; }
}
BorderCollie
的类,具有类似的Name
属性:class BorderCollie
{
public string Name { get; set; }
}
Bernese
的类,也具有Name
属性,但还有一个重写本地Equals
方法:class Bernese
{
public string Name { get; set; }
public override bool Equals(object obj)
{
if (obj is Bernese borderCollie && obj != null)
{
return this.Name == borderCollie.Name;
}
return false;
}
}
在这里,this
关键字用于引用当前的borderCollie
类。
Program.cs
文件中,你将为这些类型创建一些对象。请注意,由于你使用了顶级语句,这些声明应该在类和结构声明之上: var aGolden = new GoldenRetriever() { Name = "Aspen" };
var anotherGolden = new GoldenRetriever() { Name = "Aspen" };
var aBorder = new BorderCollie() { Name = "Aspen" };
var anotherBorder = new BorderCollie() { Name = "Aspen" };
var aBernese = new Bernese() { Name = "Aspen" };
var anotherBernese = new Bernese() { Name = "Aspen" };
Equals
方法比较这些值,并将结果分配给一些变量:var goldenComparison = aGolden.Equals(anotherGolden) ? "These Golden Retrievers have the same name." : "These Goldens have different names.";
var borderComparison = aBorder.Equals(anotherBorder) ? "These Border Collies have the same name." : "These Border Collies have different names.";
var berneseComparison = aBernese.Equals(anotherBernese) ? "These Bernese dogs have the same name." : "These Bernese dogs have different names.";
Console.WriteLine(goldenComparison);
Console.WriteLine(borderComparison);
Console.WriteLine(berneseComparison);
dotnet run
命令行运行程序,你将看到以下输出:These Golden Retrievers have the same name.
These Border Collies have different names.
These Bernese dogs have the same name.
注意
你可以在packt.link/xcWN9
找到用于这个练习的代码。
如前所述,结构体是值类型。因此,当两个相同结构的对象使用Equals
进行比较时,.NET 内部检查所有结构属性。如果这些属性具有相等的值,那么将返回true
。例如,对于Golden Retrievers
,如果你有一个FamilyName
属性,并且这个属性在两个对象之间是不同的,那么相等性比较的结果将是false
。
对于类和所有其他引用类型,相等性比较是非常不同的。默认情况下,对象引用在相等性比较上被检查。如果引用不同(而且除非两个变量被分配给相同的对象,它们将是不同的),相等性比较将返回false
。这解释了你在示例中看到的Border Collies
的结果,即两个实例的引用是不同的。
然而,有一种方法可以在引用类型中实现,叫做 Equals。给定两个对象,Equals
方法可以用于比较,遵循方法内部的逻辑。这正是伯恩山犬示例中发生的事情。
现在你已经处理了值和引用类型,你将简要探索默认值类型。在 C#中,每种类型都有一个默认值,如下表所示:
图 1.4:默认值类型表
这些默认值可以使用default
关键字分配给变量。要在变量声明中使用这个词,你必须在变量名之前显式声明变量类型。例如,考虑以下代码片段,其中你将default
值分配给两个int
变量:
int a = default;
int b = default;
在这种情况下,a
和b
都将被赋值为0
。请注意,这种情况下不能使用var
。这是因为对于隐式声明的变量,编译器需要为变量分配一个值以推断其类型。因此,以下代码片段将导致错误,因为没有设置类型,要么通过显式声明,要么通过变量赋值:
var a = default;
var b = default;
switch
语句经常被用作 if-else 结构的替代方案,如果要对三个或更多条件进行测试,则可以选择一个要执行的代码部分,例如以下情况:
switch (matchingExpression)
{
case firstCondition:
// code section
break;
case secondCondition:
// code section
break;
case thirdCondition:
// code section
break;
default:
// code section
break;
}
匹配表达式应返回以下类型之一的值:char
、string
、bool
、numbers
、enum
和object
。然后,将在匹配的case
子句中或在默认子句中对该值进行评估,如果它不匹配任何先前的子句。
重要的是要说,switch
语句中只有一个switch
部分会被执行。C#不允许从一个switch
部分继续执行到下一个。但是,switch
语句本身不知道如何停止。您可以使用break
关键字,如果只希望执行某些操作而不返回,或者如果是这种情况,返回某些内容。
此外,switch
语句上的default
关键字是在没有匹配到其他选项时执行的位置。在下一个练习中,您将使用switch
语句创建一个餐厅菜单应用程序。
在这个练习中,您将创建一个控制台应用程序,让用户从餐厅提供的食物菜单中选择。该应用程序应显示订单的确认收据。您将使用switch
语句来实现逻辑。
按照以下步骤完成此练习:
创建一个名为Exercise1_08
的新控制台项目。
现在,创建一个System.Text.StringBuilder
。这是一个帮助以多种方式构建字符串的类。在这里,您正在逐行构建字符串,以便它们可以在控制台上正确显示:
var menuBuilder = new System.Text.StringBuilder();
menuBuilder.AppendLine("Welcome to the Burger Joint. ");
menuBuilder.AppendLine(string.Empty);
menuBuilder.AppendLine("1) Burgers and Fries - 5 USD");
menuBuilder.AppendLine("2) Cheeseburger - 7 USD");
menuBuilder.AppendLine("3) Double-cheeseburger - 9 USD");
menuBuilder.AppendLine("4) Coke - 2 USD");
menuBuilder.AppendLine(string.Empty);
menuBuilder.AppendLine("Note that every burger option comes with fries and ketchup!");
Console.WriteLine(menuBuilder.ToString());
Console.WriteLine("Please type one of the following options to order:");
Console.ReadKey()
方法将其赋值给一个变量。此方法与之前使用的ReadLine()
类似,不同之处在于它读取调用方法后立即按下的键。添加以下代码:var option = Console.ReadKey();
switch
语句的时候了。在这里,将option.KeyChar.ToString()
用作switch
子句的匹配表达式。按键1
、2
、3
和4
应该分别接受汉堡
、芝士汉堡
、双层芝士汉堡
和可乐
的订单:switch (option.KeyChar.ToString())
{
case "1":
{
Console.WriteLine("\nAlright, some burgers on the go. Please pay the cashier.");
break;
}
case "2":
{
Console.WriteLine("\nThank you for ordering cheeseburgers. Please pay the cashier.");
break;
}
case "3":
{
Console.WriteLine("\nThank you for ordering double cheeseburgers, hope you enjoy them. Please pay the cashier!");
但是,任何其他输入都应被视为无效,并显示一条消息,让您知道您选择了一个无效的选项:
break;
}
case "4":
{
Console.WriteLine("\nThank you for ordering Coke. Please pay the cashier.");
break;
}
default:
{
Console.WriteLine("\nSorry, you chose an invalid option.");
break;
}
}
dotnet run --project Exercise1_08
运行程序,并与控制台交互以查看可能的输出。例如,如果您输入1
,您应该看到以下输出:Welcome to the Burger Joint.
1) Burgers and Fries – 5 USD
2) Cheeseburger – 7 USD
3) Double-cheeseburger – 9 USD
4) Coke – 2 USD
Note that every burger option comes with fries and ketchup!
Please type one of the follow options to order:
1
Alright, some burgers on the go! Please pay on the following cashier!
注意
您可以在packt.link/x1Mvn
找到此练习中使用的代码。
同样,您还应该获取其他选项的输出。您已经了解了 C#中的分支语句。在使用 C#编程时,还有另一种类型的语句经常使用,称为迭代语句。下一节将详细介绍这个主题。
迭代语句,也称为循环,是现实世界中有用的语句类型,因为您经常需要在应用程序中不断重复一些逻辑执行,直到满足某些条件,例如使用必须递增直到达到某个值的数字。C#提供了许多实现这种迭代的方法,在本节中,您将详细研究每种方法。
您将考虑的第一个迭代语句是while
语句。此语句允许 C#程序在某个布尔表达式被评估为true
时执行一组指令。它具有最基本的结构之一。考虑以下片段:
int i = 0;
while (i < 10)
{
Console.WriteLine(i);
i = i +1;
}
前面的片段显示了如何使用while
语句。请注意,while
关键字后面跟着一对括号,括号中包含一个逻辑条件;在这种情况下,条件是i
的值必须小于10
。在大括号中编写的代码将在此条件为true
时执行。
因此,前面的代码将打印i
的值,从0
开始,直到10
。这是相当简单的代码;在下一个练习中,你将使用while
语句进行一些更复杂的操作,比如检查你输入的数字是否是质数。
在这个练习中,你将使用while
循环来检查你输入的数字是否是质数。为此,while
循环将检查计数器是否小于或等于数字除以2
的整数结果。当满足这个条件时,你检查数字除以计数器的余数是否为0
。如果不是,你增加计数器并继续,直到循环条件不再满足。如果满足,这意味着数字不是false
,循环可以停止。
执行以下步骤完成这个练习:
在 VS Code 集成终端中,创建一个名为Exercise1_09
的新控制台项目。
在Program.cs
文件中创建以下方法,该方法将执行你在练习开始时介绍的逻辑:
static bool IsPrime(int number) { if (number ==0 || number ==1) return false; bool isPrime = true; int counter = 2; while (counter <= Math.Sqrt(number)) { if (number % counter == 0) { isPrime = false; break; } counter++; } return isPrime; }
Console.Write("Enter a number to check whether it is Prime: ");
var input = int.Parse(Console.ReadLine());
Console.WriteLine($"{input} is prime? {IsPrime(input)}.");
dotnet run --project Exercise1_09
并与程序交互。例如,尝试输入29
作为输入:Enter a number to check whether it is Prime:
29
29 is prime? True
如预期的那样,29
的结果是true
,因为它是一个质数。
注意
你可以在packt.link/5oNg5
找到这个练习中使用的代码。
前面的练习旨在向你展示一个带有一些更复杂逻辑的while
循环的简单结构。它检查一个名为input
的数字,并打印出它是否是一个质数。在这里,你已经看到了再次使用break
关键字来停止程序执行。现在继续学习跳转语句。
在循环中使用的一些其他重要关键字也值得一提。这些关键字称为跳转语句,用于将程序执行转移到另一个部分。例如,你可以将IsPrime
方法重写如下:
static bool IsPrimeWithContinue(int number) { if (number == 0 || number ==1) return false; bool isPrime = true; int counter = 2; while (counter <= Math.Sqrt(number)) { if (number % counter != 0) { counter++; continue; } isPrime = false; break; } return isPrime; }
在这里,你已经颠倒了逻辑检查。不再检查余数是否为零然后中断程序执行,而是检查余数是否不为零,如果是,则使用continue
语句将执行传递到下一个迭代。
现在看看如何使用另一个特殊关键字goto
重写这个:
static bool IsPrimeWithGoTo(int number) { if (number == 0 || number ==1) return false; bool isPrime = true; int counter = 2; while (counter <= Math.Sqrt(number)) { if (number % counter == 0) { isPrime = false; goto isNotAPrime; } counter++; } isNotAPrime: return isPrime; }
goto
关键字可以用来从代码的一个部分跳转到另一个由所谓的标签定义的部分。在这种情况下,标签被命名为isNotAPrime
。最后,看一下写这个逻辑的最后一种方法:
static bool IsPrimeWithReturn(int number)
{
if (number == 0 || number ==1) return false;
int counter = 2;
while (counter <= Math.Sqrt(number))
{
if (number % counter == 0)
{
return false;
}
counter ++;
}
return true;
}
现在,不再使用break
或continue
来停止程序执行,而是简单地使用return
来中断循环执行,因为已经找到了你要找的结果。
do-while
循环与前一个循环类似,但有一个细微的区别:它至少执行一次逻辑,而简单的while
语句如果条件在第一次执行时不满足可能永远不会执行。它有以下结构:
int t = 0;
do
{
Console.WriteLine(t);
t++;
} while (t < 5);
在这个例子中,你从0
开始写入t
的值,并在它小于5
时不断递增。在跳转到下一个循环类型之前,学习一个叫做数组的新概念。
数组是一种用于存储相同类型的许多对象的数据结构。例如,下面的例子是一个声明为整数数组的变量:
int[] numbers = { 1, 2, 3, 4, 5 };
关于数组的第一要点是它们有一个固定的容量。这意味着数组的长度在创建时被定义,并且这个长度不能改变。长度可以通过各种方式确定。在前面的例子中,长度是通过计算数组中对象的数量来推断的。然而,创建数组的另一种方式是这样的:
var numbers = new int[5];
在这里,您正在创建一个具有 5 个整数容量的数组,但您没有为数组元素指定任何值。当创建任何数据类型的数组时,而没有向其添加元素时,将为数组的每个位置设置该值类型的默认值。例如,请考虑以下图:
图 1.5:未分配索引的值类型数组
前面的图表显示,当您创建一个包含五个元素的整数数组时,而没有为任何元素分配值时,数组会自动填充每个位置的默认值。在这种情况下,默认值为 0。现在考虑以下图表:
图 1.6:具有固定大小和仅分配一个索引的引用类型数组
在前面的示例中,您创建了一个包含五个对象的数组,并将"Hello"
字符串值分配给索引 1 处的元素。数组的其他位置会自动分配对象的默认值,即 null。
最后值得注意的是,所有数组都有索引,它指的是单个数组元素的位置。第一个位置将始终具有索引 0。因此,大小为 n 的数组的位置可以从索引 0 到 n-1 指定。因此,如果调用 numbers[2],这意味着您正在尝试访问 numbers 数组中位置 2 的元素。
for 循环执行一组指令,同时匹配指定条件的布尔表达式。就像 while 循环一样,跳转语句可以用于停止循环执行。它具有以下结构:
for (initializer; condition; iterator)
{
[statements]
}
初始化语句在循环开始之前执行。它用于声明和分配一个只在循环范围内使用的局部变量。
但在更复杂的情况下,它也可以用于组合其他语句表达式。条件指定一个布尔条件,指示循环何时应继续或退出。迭代器通常用于增加或减少初始化部分中创建的变量。考虑以下示例,其中使用 for 循环打印整数数组的元素:
int[] array = { 1, 2, 3, 4, 5 };
for (int j = 0; j < array.Length - 1; j++)
{
Console.WriteLine(array[j]);
}
在此示例中,创建了一个初始化变量 j,最初分配为 0。当 j 小于数组长度减 1 时(请记住,索引始终从 0 开始),for 循环将继续执行。每次迭代后,j 的值增加 1。通过这种方式,for 循环遍历整个数组并执行给定的操作,即打印当前数组元素的值。
C#还允许使用嵌套循环,即循环内的循环,正如您将在下一个练习中看到的。
在这个练习中,您将执行最简单的排序算法之一。冒泡排序包括遍历数组中的每一对元素,并在它们无序时交换它们。最终,期望是得到一个按升序排序的数组。您将使用嵌套的 for 循环来实现这个算法。
首先,要对其进行排序的数组应作为参数传递给此方法。对于该数组的每个元素,如果当前元素大于下一个元素,则它们的位置应该被交换。这种交换是通过将下一个元素的值存储在临时变量中,将当前元素的值分配给下一个元素,最后用临时存储的值设置当前元素的值来实现的。一旦第一个元素与所有其他元素进行比较,就会开始对第二个元素进行比较,依此类推,直到最终数组排序完成。
以下步骤将帮助您完成此练习:
dotnet new console -n Exercise1_10
Program.cs
文件中,创建实现排序算法的方法。添加以下代码:static int[] BubbleSort(int[] array) { int temp; // Iterate over the array for (int j = 0; j < array.Length - 1; j++) { // If the last j elements are already ordered, skip them for (int i = 0; i < array.Length - j - 1; i++) { if (array[i] > array[i + 1]) { temp = array[i + 1]; array[i + 1] = array[i]; array[i] = temp; } } } return array; }
int[] randomNumbers = { 123, 22, 53, 91, 787, 0, -23, 5 };
BubbleSort
方法,将数组作为参数传递,并将结果分配给一个变量,如下所示:int[] sortedArray = BubbleSort(randomNumbers);
Console.WriteLine("Sorted:");
for (int i = 0; i < sortedArray.Length; i++)
{
Console.Write(sortedArray[i] + " ");
}
dotnet run --project Exercise1_10
命令运行程序。您应该在屏幕上看到以下输出:Sorted:
-23 0 5 22 53 91 123 787
注意
您可以在packt.link/cJs8y
找到用于此练习的代码。
在这个练习中,您使用了在最后两节中学到的两个概念:数组和 for 循环。您操作了数组,通过索引访问它们的值,并使用 for 循环来移动这些索引。
还有另一种遍历数组或foreach
语句的方法。您将在下一节中探讨这一点。
foreach
语句为集合的每个元素执行一组指令。就像for
循环一样,break
,continue
,goto
和return
关键字也可以与foreach
语句一起使用。考虑以下示例,在该示例中,您遍历数组的每个元素并将其写入控制台作为输出:
var items = new int[] { 1, 2, 3, 4, 5 };
foreach (int element in items)
{
Console.WriteLine(element);
}
前面的代码段将数字1
到5
打印到控制台。您可以使用foreach
语句处理的不仅仅是数组;它们还可以与列表,集合和跨度一起使用,这些是稍后将在后面的章节中介绍的其他数据结构。
到目前为止,您一直在创建大部分与 CPU 和内存交互的程序。本节将重点放在 I/O 操作上,即物理磁盘上的输入和输出操作。这种操作的一个很好的例子是文件处理。
C#有几个类可帮助您执行 I/O 操作。其中一些如下:
File
:此类提供了用于文件操作的方法,即在磁盘上读取,写入,创建,删除,复制和移动文件。
Directory
:与File
类一样,此类包括用于在磁盘上创建,移动和枚举目录和子目录的方法。
Path
:这提供了处理文件和目录在磁盘上的绝对路径和相对路径的实用程序。相对路径始终与应用程序正在执行的当前目录内的某个路径相关联,而绝对路径是指硬盘内的绝对位置。
DriveInfo
:这提供有关磁盘驱动器的信息,例如Name
,DriveType
,VolumeLabel
和DriveFormat
。
您已经知道文件大多是位于硬盘某处的一些数据集,可以通过某个程序打开以进行读取或写入。当您在 C#应用程序中打开文件时,您的程序通过通信通道将文件作为字节序列读取。这个通信通道称为流。流可以是两种类型:
输入流用于读取操作。
输出流用于写操作。
Stream
类是 C#中的一个抽象类,它使得关于这个字节流的常见操作成为可能。对于硬盘上的文件处理,您将使用FileStream
类,专门设计用于此目的。这个类的两个重要属性是FileAccess
和FileMode
。
这是一个enum
,为您提供了在打开指定文件时选择访问级别的选项:
Read
:这以只读模式打开文件。
ReadWrite
:这以读写模式打开文件。
Write
:这以只写模式打开文件。这很少使用,因为通常您会在写入时进行一些读取。
这是一个enum
,指定可以在文件上执行的操作。它应该与访问模式一起使用,因为某些模式只适用于某些访问级别。看一下选项,如下所示:
Append
:当你想在文件末尾添加内容时使用。如果文件不存在,将创建一个新文件。对于这个操作,文件必须具有写入权限;否则,任何读取尝试都会失败并抛出NotSupportedException
异常。异常是一个重要的概念,将在本章后面介绍。
Create
:用于创建新文件或覆盖现有文件。对于这个选项,也需要写入权限。在 Windows 中,如果文件存在但被隐藏,将抛出UnauthorizedAccessException
异常。
CreateNew
:这类似于Create
,但用于创建新文件,也需要写入权限。但是,如果文件已经存在,将抛出IOException
异常。
Open
:顾名思义,这种模式用于打开一个文件。文件必须具有读取或读取和写入权限。如果文件不存在,将抛出FileNotFoundException
异常。
OpenOrCreate
:这类似于Open
,除非文件不存在,否则会创建一个新文件。
在这个练习中,你将从逗号分隔值(CSV)文件中读取文本。CSV 文件简单地包含由字符串表示并由冒号或分号分隔的数据。
完成这个练习,执行以下步骤:
dotnet new console -n Exercise1_11
Exercise1_11
项目文件夹位置,创建一个名为products.csv
的文件,并将以下内容粘贴到其中:Model;Memory;Storage;USB Ports;Screen;Condition;Price USD
Macbook Pro Mid 2012;8GB;500GB HDD;USB 2.0x2;13" screen;Refurbished;400
Macbook Pro Mid 2014;8GB;512GB SSD;USB 3.0x3;15" screen;Refurbished;750
Macbook Pro Late 2019;16GB;512GB SSD;USB 3.0x3;15" screen;Refurbished;1250
Program.cs
文件,并用以下内容替换它的内容:using System; using System.IO; using System.Threading.Tasks; namespace Exercise1_11 { public class Program { public static async Task Main() { using (var fileStream = new FileStream("products.csv", FileMode.Open, FileAccess.Read)) { using (var reader = new StreamReader(fileStream)) { var content = await reader.ReadToEndAsync(); var lines = content.Split(Environment.NewLine); foreach (var line in lines) { Console.WriteLine(line); } } } } } }
dotnet run
,你将得到一个与你创建的 CSV 文件内容相同的输出。注意
你可以在packt.link/5flid
找到这个练习所使用的代码。
这个练习有一些非常有趣的结果,你将逐步学习。首先,你使用FileStream
类打开了一个文件。这允许你从文件中流出字节,具有两个特殊属性,即FileMode
和FileAccess
。它将返回一个StreamReader
类。这个类使你能够将这些字节读取为文本字符。
还要注意,你的Main
方法从void
变成了async
Task。此外,使用了await
关键字,用于异步操作。你将在接下来的章节中学到更多关于这些主题的知识。现在,你只需要知道异步操作是指不阻塞程序执行的操作。这意味着你可以在它们被读取时输出行;也就是说,你不必等待它们全部被读取。
在下一节中,学习处理文件、数据库和网络连接的特殊关键字。
前面练习的另一个特殊之处是using
关键字。它是一个用于清理内存中未管理资源的关键字。这些资源是处理一些操作系统资源的特殊对象,如文件、数据库和网络连接。它们被称为特殊,因为它们执行所谓的 I/O 操作;也就是说,它们与机器的真实资源进行交互,如网络和硬盘驱动器,而不仅仅是内存空间。
C#中对象使用的内存由一个叫做垃圾收集器的东西处理。默认情况下,C#处理堆栈和堆中的内存空间。唯一不执行此清理的对象类型被称为未管理对象。
清理这些对象从内存中意味着资源将被释放,以便被计算机中的另一个进程使用。这意味着一个文件可以被另一个文件处理,数据库连接可以再次被连接池使用,依此类推。这些类型的资源被称为可处置资源。每当你处理一个可处置资源时,你可以在创建对象时使用using
关键字。然后,编译器知道当using
语句关闭时,它可以自动释放这些资源。
在这个练习中,你将再次使用FileStream
类将一些文本写入 CSV 文件中。
按照以下步骤完成这个练习:
dotnet new console -n Exercise1_12
在你电脑上的一个首选位置,从上一个练习中复制products.csv
文件,并将其粘贴到这个练习的文件夹中。
在Program.cs
中,创建一个名为ReadFile
的方法,该方法将接收一个FileStream
文件,并迭代文件行以将结果输出到控制台:
static async Task ReadFile(FileStream fileStream)
{
using (var reader = new StreamReader(fileStream))
{
var content = await reader.ReadToEndAsync();
var lines = content.Split(Environment.NewLine);
foreach (var line in lines)
{
Console.WriteLine(line);
}
}
}
StreamWriter
打开products.csv
文件,并添加一些更多的信息,如下所示: using (var file = new StreamWriter("products.csv", append: true))
{
file.Write("\nOne more macbook without details.");
}
using (var fileStream = new FileStream("products.csv", FileMode.Open,
FileAccess.Read))
{
await ReadFile(fileStream);
}
dotnet run --project Exercise1_12
,你将能够看到你刚刚创建的 CSV 文件的内容,以及你刚刚追加的行:Model;Memory;Storage;USB Ports;Screen;Condition;Price USD
Macbook Pro Mid 2012;8GB;500GB HDD;USB 2.0x2;13" screen;Refurbished;400
Macbook Pro Mid 2014;8GB;512GB SSD;USB 3.0x3;15" screen;Refurbished;750
Macbook Pro Late 2019;16GB;512GB SSD;USB 3.0x3;15" screen;Refurbished;1250
One more macbook without details.
注意,每次运行,程序都会追加一行新的内容,所以你会看到添加了更多的行。
注意
你可以在packt.link/dUk2z
找到这个练习使用的代码。
有时你的程序会在某个时候执行失败,并且可能不提供输出。这种情况被称为异常错误。下一节详细介绍了这种错误。
异常表示程序在某个时刻由于某种原因无法执行,并且可以由代码本身或.NET 运行时引发。通常,异常是严重的失败,甚至可能终止程序的执行。幸运的是,C#提供了一种特殊的处理异常的方式,即try/catch
块:
try
{
// some logic that might throw an exception
}
catch
{
// error handling
}
在try
子句中,调用可能引发异常的代码,在catch
子句中,你可以处理引发的异常。例如,考虑以下例子:
double Divide(int a, int b) => a/b;
这个方法接受两个整数,并返回它们之间的除法结果。然而,如果b
是0
会发生什么呢?在这种情况下,运行时会抛出System.DivideByZeroException
,表示不可能执行除法。你如何在真实世界的程序中处理这个异常?你将在下一个练习中探讨这个问题。
在这个练习中,你将创建一个控制台应用程序,从你那里获取两个输入,将第一个数字除以第二个数字,并输出结果。如果你输入了一个无效的字符,应用程序应该抛出一个异常,并且所有这些都应该在程序逻辑内部处理。
执行以下步骤完成这个练习:
在 VS Code 集成终端中,创建一个名为Exercise1_13
的新控制台应用程序。
在Program.cs
文件中创建以下方法:
static double Divide(int a, int b)
{
return a / b;
}
false
赋给它作为初始值:bool divisionExecuted = false;
while
循环,检查除法是否成功进行。如果成功,程序应该终止。如果没有,程序应该提示你输入有效数据,并再次执行除法。添加以下代码来实现这一点:while (!divisionExecuted) { try { Console.WriteLine("Please input a number"); var a = int.Parse(Console.ReadLine()); Console.WriteLine("Please input another number"); var b = int.Parse(Console.ReadLine()); var result = Divide(a, b); Console.WriteLine($"Result: {result}"); divisionExecuted = true; } catch (System.FormatException) { Console.WriteLine("You did not input a number. Let's start again ... \n"); continue; } catch (System.DivideByZeroException) { Console.WriteLine("Tried to divide by zero. Let's start again ... \n"); continue; } }
dotnet run
命令执行程序并与控制台交互。尝试插入字符串而不是数字,看看你得到什么输出。看下面的输出作为一个例子:Please input a number
5
Please input another number
0
Tried to divide by zero. Let's start again …
Please input a number
5
Please input another number
s
You did not input a number. Let's start again …
Please input a number
5
Please input another number
1
Result: 5
注意
你可以在packt.link/EVsrJ
找到这个练习使用的代码。
在这个练习中,你处理了两种异常,分别是:
int.Parse(string str)
方法在无法将string
变量转换为整数时抛出System.FormatException
。
double Divide(int a, int b)
方法在 b 为 0 时抛出System.DivideByZeroException
。
现在你已经看到了异常是如何处理的,重要的是要注意一个经验法则,这将帮助你在 C#的旅程中,那就是你应该只捕获你能够或者需要处理的异常。只有在少数情况下真正需要异常处理,如下所示:
当你想要掩盖一个异常,也就是捕获它并假装什么都没发生。这被称为异常抑制。当抛出的异常不影响程序流程时,就应该发生这种情况。
当你想要控制程序的执行流程以执行一些替代操作时,就像你在前面的练习中所做的那样。
当你想要捕获一种异常并将其作为另一种类型抛出时。例如,当与你的 Web API 通信时,你可能会看到一个HttpException
类型的异常,表示目标不可达。你可以在这里使用自定义异常,比如IntegrationException
,以更清楚地指示它发生在你的应用程序的某个部分,该部分与外部 API 进行一些集成。
throw
关键字也可以用于有意地在某些情况下停止程序的执行流程。例如,假设你正在创建一个Person
对象,并且Name
属性在创建时不应为null
。你可以在这个类上强制使用System.ArgumentException
或System.ArgumentNullException
,就像下面的代码片段中使用ArgumentNullException
一样:
class Person
{
Person(string name)
{
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullException(nameof(name));
Name = name;
}
String Name { get ; set; }
}
在这里,如果name
参数的值为null
或者只输入空格字符,就会抛出ArgumentNullException
,程序无法成功执行。空值/空格条件是通过IsNullOrWhiteSpace
函数来检查的,该函数可以用于字符串变量。
现在是时候通过一个活动来练习你在之前章节学到的所有知识了。
要完成这个活动,你需要使用你在本章中学到和练习过的概念来创建一个猜数字游戏。在这个游戏中,首先必须生成一个从 1 到 10 的随机数,不需要输出到控制台。然后控制台应提示用户输入一个数字,然后猜测生成的随机数是多少,用户最多有五次机会。
在每次输入错误时,应显示一个警告消息,让用户知道他们还剩下多少次机会,如果所有五次机会都用于错误猜测,程序将终止。然而,一旦用户猜对了,程序将在终止之前显示一个成功消息。
以下步骤将帮助你完成这个活动:
numberToBeGuessed
的变量,它被赋予 C#中的一个随机数。你可以使用以下代码片段来实现:new Random().Next(0, 10)
这会为你生成一个在0
和10
之间的随机数。如果你想让游戏变得更难一些,你可以用一个更大的数字来替换10
,或者用一个更小的数字来让它变得更容易,但是在这个活动中,你将使用10
作为最大值。
创建一个名为remainingChances
的变量,用于存储用户剩余的机会数。
创建一个名为numberFound
的变量,并将其赋值为false
。
现在,创建一个while
循环,当还有一些机会剩余时执行。在这个循环中,添加代码来输出剩余的机会次数,直到猜对为止。然后,创建一个名为number
的变量,用于接收正确的猜测,并在numberFound
变量中赋值true
。如果没有猜对,剩余机会次数应减少 1。
最后,添加代码来告知用户他们是否猜对了数字。如果他们猜对了,可以输出类似于恭喜!你用{remainingChanges}次机会猜对了数字!
的内容。如果他们用完了机会,输出你没有机会了。数字是{numberToBeGuessed}。
。
注意
这个活动的解决方案可以在packt.link/qclbF
找到。
本章概述了 C#的基础知识,以及使用它编写程序的样子。你探索了从变量声明、数据类型和基本算术和逻辑运算符到文件和异常处理的一切。你还探索了 C#在处理值类型和引用类型时如何分配内存。
在本章的练习和活动中,你能够解决一些现实世界的问题,并想出可以用这种语言及其资源实现的解决方案。你学会了如何在控制台应用程序中提示用户输入,如何在系统中处理文件,最后,如何通过异常处理处理意外输入。
下一章将涵盖面向对象编程的基本知识,深入探讨类和对象的概念。你还将了解编写清晰、简洁、易于维护的代码的重要性,以及编写此类代码的原则。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。