文字教程 : C#从入门到精通教程
视频教程: 跟着sunny老师学C#
源码 : gitee仓库
写在前面
笔者是一个工作在一线的程序员。
我曾看过很多关于编程的书籍和教程,以C#最为甚之,从视频到博客,从官方文档到源码,所览之内容,使我收获颇丰。然而笔者实在不为称道的记忆力,看到后页内容便忘却前页,无奈使然,只能做些笔记。内容记录多了,我就分享到网络,虽内容粗鄙,逻辑混乱,蒙乘诸君抬举,有人收藏和留言,给我鼓励和信息。
历时一年,笔者又记录了15万字流水账,年底应该会有20万字。也不怕诸君笑话,文章内容俗套,毫无创新可言,望酌情读之,如能得诸君指点一二,便是暗室逢灯,枯木逢春。
笔者四体不勤,书籍未成而先发布,各位看官且慢慢听我到来。。。。
跟着sunny老师学C#
- 写在前面
- 1.序言
- 2.计算机编程绪论
- 2.1.计算机的工作原理
- 2.2.C#程序在计算机运行过程
- 2.3.C#中不同的应用程序
- 2.4.面向对象编程和面向过程编程
- 2.5..net框架介绍
- 2.6..NET 框架的架构与组件
- 2.7.C#语言介绍
- 2.8.创建第一个console应用程序
- 2.9.C#从入门到精通学习线路
- 2.10.C#开发者的最佳实践
- 2.10.1.最佳实践1:命名要合理
- 2.10.2.最佳实践2:每个文件仅包含一个类
- 2.10.3.最佳实践3:使用属性而不是公共变量
- 2.10.4.最佳实践4:方法应只做一件事
- 2.10.5.最佳实践5:仅在必要时使用公共修饰符
- 2.10.6.最佳实践6:保持代码简单
- 2.10.7.最佳实践7:保持一致性
- 2.10.8.最佳实践8:在if语句中使用大括号
- 2.10.9.最佳实践9:使用字符串插值
- 2.10.10.最佳实践10:避免使用全局变量
- 2.10.11.最佳实践11:注释和文档
- 2.10.12.最佳实践12:不要信任用户输入
- 2.10.13.最佳实践13:在编写代码之前进行计划
- 2.10.14.最佳实践14:不要过度设计代码
- 3.C#基础知识
- 3.1.C# 程序的基本结构
- 3.2.Console 类的方法和属性
- 3.3.C#中的数据类型
- 3.4.特殊的字符串
- 3.5.C#中的字面量
- 3.6.C#中的类型转换
- 3.7.C# 中的操作符
- 3.8.控制流语句
- 3.9.方法
- 3.10.函数的递归调用
- 3.11.字符串
- 3.12.静态类和静态方法
- 3.13.常量和只读变量
- 3.14.C# 中的属性
- 3.15.Checked 和 Unchecked 关键字
- 3.16.栈和堆内存
- 3.17.装包与拆包(装箱和拆箱)
- 4.面向对象编程(OOPs)
- 5.异常处理
- 6.委托、事件、Lambda表达式
- 7.泛型编程
1.序言
我用过C、C++、Python、JavaScript、Python、Matlab、qt等多种编程语言和工具,我也知道不同的需求要上不同的语言,需要不同的平台,但这些语言都或多或少的有些地方让我不舒服,有的的是语言本身难,有的呢则是语言松散、版本之间依赖性差,有的呢语言设计有缺陷,但自从我遇到C#,我基本没有发现这种语言的缺陷,新版的visual studio2022企业版也让我基本找不到缺陷,C#搭配visual studio2022真的是无敌。你可能不能理解我表达的意思,你就想一想,你手机上的软件,哪个不是广告多,就是设计反人类,让用户体验不好,但有一个软件除外—微信。我基本找不到微信的任何缺点,这个堪称完美的产品,让我这个鸡蛋里挑骨头的人都闭嘴,C#在我心中的地位就是这样。
我想与大家分享15个理由,分享我作为C#开发者的经验,为什么我认为C#是世界上最好的编程语言,以及为什它值得你去学习它。
一、C#的简单、可读性和易用性
作为开发者,我们知道大部分时间都花在阅读和理解代码上(包括和客户扯皮),而不是编写代码。因此,可读性成为我考虑编程语言时最重要的品质之一。C#从设计之初就注重简单性和可读性,这是其核心设计原则之一。C#不允许直接使用裸指针访问内存(你非要做也可以),也不提供多重类继承,简化了多重继承带来的变态问题。C#提供了垃圾收集器来自动管理内存,使得开发者无需过多关注内存管理问题。
二、C#对开发者生产力的关注
C#自发布以来,一直是一个强类型语言,强类型语言要求变量在使用前必须明确声明其类型,并且在变量的生命周期内,这个类型通常是不能改变的。这种特性有助于编译器在编译时捕获许多错误,从而提高代码的稳定性和可维护性。Visual Studio和Visual Studio Code是C#开发者使用的最佳软件开发环境之一,它们提供了强大的代码分析和代码生成功能,能够极大提升开发者的生产力。
三、C#的多范式编程能力
C#从一个强面向对象的语言逐渐发展成为一个支持命令式(安装扩展包、数据库迁移等)、声明式、泛型和函数式编程风格的多范式语言。这种融合使得C#能够从其他编程语言中汲取精华,并以一种非常连贯的方式提供它们。
四、C#的灵活性和通用性
C#是一种非常灵活且通用的语言,允许你开发各种系统。你可以使用C#编写控制台应用程序、桌面应用程序、Windows服务、Web服务和Web应用程序、原生移动应用程序、AI应用程序、分布式和云应用程序等。C#技能的回报是最大化的,因为你只要学会一门编程语言,几乎可以用它构建任何类型的应用程序。
五、C#运行在.NET运行环境上
C#不是孤立设计的,而是作为整体.NET框架项目的一部分。CLR(公共语言运行时)是一个卓越的工程成就,提供了内存管理、即时编译、程序集版本控制和加载、安全性、线程同步、异常处理、公共类型系统、属性、与托管代码的互操作性等功能。.NET Core并不是在.net Framework的基础上开发的,而是零开始开发,并抛弃了.Net Framework臃肿的框架,.NET Core采用了包的方式提供运行环境,需要什么环境,就联网下载什么包,简便而轻盈。
六、C#的跨平台能力
最初,C#是专为Windows开发者设计的语言,因为.Net Framework框架与Microsoft操作系统紧密耦合。但如今,这种情况已不复存在。.NET被设计为可以在windows、Linux和Mac在内的多个操作系统上运行,MAUI(Multi-platform App UI)提供了在包括Android和iOS在内的移动平台上原生运行C#代码的能力。
七、C#的成熟度、流行度和活跃的开发状态
到2023年,C#已经是经历24年发展你的成熟语言。C#常年霸榜全球最受欢迎的五大编程语言之一,能和C、C++这些编程语言并列,受欢迎程度可想而知。
八、C#代码是开源的
C#所有的代码现在完全在GitHub上开源,这非常了不起(Linux也是)。好像是从C# 7.0开始,这之后都是使用开源模式开发的,未来的版本也将继续以这种方式开发。每个人都可以通过在官方C# GitHub页面上创建问题来提供反馈和提出新功能。
九、C#社区生态非常好
我之前非常喜欢Python的社区。因为总是有无私奉献的人,把他们开发的各种工具包贡献出来。现在我也非常喜欢C#和.NET社区,社区里也有一些大神和公司无私奉献他们的代码和框架。社区文化开源有很多好处,你自己不想写的包就可以去社区上搜一搜,基本上都有现成的东西,对于想偷懒的开发者来说,C#的是再好不过。
十、C#的文档完善性
C#的官方文档写得非常好,是学习C#的最佳起点。不知道你们有没有去微软的官网去看一下C#官方文档,还有写的比这个更详细的文档吗?
十一、C#内置设计模式和最佳实践
C#直接在语言中嵌入了许多重要的设计模式,有助于以非常优雅的方式正确实现设计模式。有时,你甚至不知道自己实际上正在使用设计模式,这正是它应该有的样子。举个例子,你肯定使用过List list = new List,你知道这个是泛型编程吗,可能都没有关注,还有就是LINQ查询,这些语法糖真的好用到爆,只有你用过其他编程语言,你才知道这些设计模式有多好,你多么希望其他编程语言也有类似的功能。
十二、C#可以利用广泛的库集合
.NET框架提供的功能强大基类库,如使用文件系统、通过网络发送和接收数据、执行数学和加密操作等,一行代码解决问题。此外,NuGet是.NET的默认包管理器,提供了超过百万万个独特包,可立即在你的C#应用程序中使用,你就想想自己用Python包时有多爽,且C#的包质量更高,兼容性也是Python比不了的。
十三、C#可以运行速度非常快
尽管C#最初的设计目标并不是以性能为重点,但随着时间的推移,C#引入了许多功能来帮助开发者优化性能和内存分配。这些功能包括结构体、指针、固定语句、值元组、值任务、引用结构体等。C#的运行速度和C、C++相差无几,但远比java、Python快多了。
十四、你可以骂微软,但永远不要怀疑它的眼光
最近这两年,最流行的就是大语言模型,而大语言模型的起点都源自ChatGpt, 你知道ChatGpt最大的股东是谁吗?就是这个千夫所指、万人唾弃的微软,我们可以骂微软,但另一方又不得不佩服它的眼光之毒辣,或许我们不是旗手,我们不知道未来的路该怎么走,但是我想着跟着巨人的后面,应该是当下最好的选择。
2.计算机编程绪论
2.1.计算机的工作原理
计算机工作方式可以总结为四个步骤:输入、存储、处理、输出。
2.1.1.输入(Input)
① 用户通过输入设备(如键盘、鼠标)向计算机输入数据和命令。
② 输入设备将这些数据转换成计算机能够理解的数字信号。
2.1.2.存储(Storage)
① 输入的数据首先被存储在计算机的内存(RAM)中。内存是高速的临时存储区域,CPU可以快速访问存储在这里的数据和指令。
② 如果需要长期存储,数据会被写入硬盘(HDD)或固态硬盘(SSD)。
2.1.3.处理(Processing)
中央处理器(CPU)是计算机的“大脑”,负责执行程序中的指令。
① CPU从内存中读取指令,并依次执行这些指令。
② CPU包含运算器和控制器。运算器负责执行算术和逻辑运算,控制器负责指挥计算机的各个部分协调工作。
2.1.4.输出(Output)
① 处理完成后,计算结果通过输出设备(如显示器、打印机)展示给用户。
② 输出设备将计算机内部的数字信号转换成用户可以理解的信息。
根据上述步骤,我们绘制出计算机完整的工作流程:
2.2.C#程序在计算机运行过程
在理解C#代码是如何在计算机运行原理之前,我们先想象一个场景:
首先,想象一下,一个程序员(就是你啦)坐在电脑前,喝着已经冷掉的咖啡,眼睛盯着屏幕,眼睛里发着呆(哈哈)。。。
你在Visual Studio中编写C#代码,写完代码后,点击“编译”或“构建”按钮。这时,编译器会检查你的代码是否有语法错误。如果有,它会告诉你哪里出错了,你需要修正这些错误。
当你的代码没有语法错误时,编译器会把你的C#代码转换成一种叫做中间语言(Intermediate Language,简称IL)的代码,通常是.dll(动态链接库)或.exe(可执行文件)。但这也是一个相对较低级的代码,计算机还是不能直接执行这些机器代码。
当你又双击exe时,操作系统会把你的.exe文件加载到内存中,然后CLR开始工作。CLR首先从内存中读取IL代码,并把它转换成机器代码。这一步叫做“即时编译”(Just-In-Time Compilation,简称JIT编译)。JIT编译器会在程序运行时逐步将IL代码编译成机器代码,这样计算机就能理解并执行了。
注意:C#程序并不会直接在你的计算机上运行,而是需要一个特殊的运行时环境,叫做“公共语言运行库”(Common Language Runtime,简称CLR)。CLR是.NET框架的一部分,它类似于一个虚拟机,负责管理和执行你的程序。
2.2.1.编写源代码
开发人员使用编程语言(如C#)编写源代码。
源代码是人类可读的文本,描述了程序的逻辑和功能。
2.2.2.编译(Compile)
源代码需要经过编译器转换成机器码(机器语言),计算机才能执行。
2.2.3.编译器将源代码翻译成CPU能够理解的二进制代码。
在C#中,编译器生成的是中间语言(IL)代码,这是一种与特定平台无关的中间代码。
2.2.4.加载(Load)
编译后的程序被加载到内存中准备执行。
在C#中,这个过程由运行时(Runtime)完成,例如.NET运行时。
2.2.5.执行(Execute)
运行时(Runtime)将中间语言代码进一步编译成特定平台的机器码(即时编译,JIT)。
CPU执行这些机器码指令,完成程序的功能。
2.2.6.输出结果
程序执行完成后,将结果输出给用户。
输出可以是显示在屏幕上的信息、打印的文件、存储在磁盘上的数据等。
2.3.C#中不同的应用程序
C# 是一种功能强大的语言,适用于各种类型的应用程序开发。无论是简单的控制台应用程序,复杂的桌面软件,现代Web应用程序,丰富的图形用户界面应用,跨平台的移动应用,还是高性能的游戏开发,C# 都能满足开发者的需求,提供了丰富的工具和框架。通过选择合适的框架和工具,开发者可以充分利用C#的优势,创建高效、可靠和用户友好的应用程序。
2.3.1.控制台应用程序 (Console Applications)
这些是最简单的C#应用程序,通常用于学习和测试基本编程概念。控制台应用程序在命令行界面中运行,用户通过命令行输入和输出数据。这类应用程序不需要图形用户界面,非常适合快速开发和测试逻辑算法。
示例代码:
using System;
namespace HelloWorld
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello, World!");
}
}
}
2.3.2.Windows 窗体应用程序 (Windows Forms Applications)
这些是带有图形用户界面的桌面应用程序,使用Windows Forms库创建窗口、按钮、文本框等用户界面元素。适用于开发传统的桌面软件,如文本编辑器、图像处理软件等。
示例代码:
using System;
using System.Windows.Forms;
namespace WindowsFormsApp
{
public class MainForm : Form
{
public MainForm()
{
Button button = new Button();
button.Text = "Click Me!";
button.Click += (sender, e) => MessageBox.Show("Hello, World!");
Controls.Add(button);
}
}
static class Program
{
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new MainForm());
}
}
}
2.3.3. ASP.NET Web 应用程序
用于创建动态Web应用程序和Web服务。ASP.NET提供了多种框架(如ASP.NET MVC和ASP.NET Core),用于构建现代Web应用。适用于开发企业级Web应用、电子商务平台、社交网络等。
示例代码(ASP.NET Core):
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace WebApp
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}
2.3.4.WPF 应用程序 (Windows Presentation Foundation Applications)
用于创建具有丰富图形用户界面的桌面应用程序。WPF使用XAML来定义用户界面,适用于开发需要复杂UI和图形效果的应用程序,如设计软件、游戏等。
示例代码:
<Window x:Class="WpfApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Button Name="button" Content="Click Me!" Click="button_Click" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Grid>
</Window>
using System.Windows;
namespace WpfApp
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void button_Click(object sender, RoutedEventArgs e)
{
MessageBox.Show("Hello, World!");
}
}
}
2.3.5.移动应用程序 (Mobile Applications)
使用Xamarin,可以使用C#开发跨平台的移动应用程序,适用于iOS和Android平台。Xamarin允许开发者使用单一代码库,创建原生移动应用。
示例代码(Xamarin.Forms):
using Xamarin.Forms;
namespace MobileApp
{
public class App : Application
{
public App()
{
MainPage = new ContentPage
{
Content = new StackLayout
{
VerticalOptions = LayoutOptions.Center,
Children =
{
new Label
{
HorizontalTextAlignment = TextAlignment.Center,
Text = "Welcome to Xamarin.Forms!"
}
}
}
};
}
protected override void OnStart() { }
protected override void OnSleep() { }
protected override void OnResume() { }
}
}
2.3.6.游戏开发 (Game Development)
C#也被用于游戏开发,尤其是使用Unity引擎。Unity支持C#脚本编写游戏逻辑,广泛应用于开发2D和3D游戏。
示例代码(Unity C#脚本):
using UnityEngine;
public class HelloWorld : MonoBehaviour
{
void Start()
{
Debug.Log("Hello, World!");
}
}
2.4.面向对象编程和面向过程编程
2.4.1. 面向过程编程
当你刚下班回到家中时,你那知性漂亮的老婆,穿着拖鞋,跳上前抱着你亲了一口,“亲爱的,辛苦了!”,她拿着你的外套,轻轻的挂在衣钩架上。你在鞋架旁边已经脱去鞋子,手里正在扯下袜子,你看到妻子的脚上穿着黑白相间的袜子,她在家里虽然经常穿着拖鞋,但是还是喜欢要穿着袜子。她问到:“亲爱的,你今晚想吃什么呢?我给你做”。现代欧式风格的桃木桌上放着上周她买的一本食谱,食谱纸张摸上去很有质感,每张食谱外面还有一层膜,站在远处看时,有意无意的反射着客厅侧边的大吊灯的微光,好像婴儿吃饱了奶,眼睛呆呆的看着一个方向,时不时眨巴一下。食谱打开后是一张张色彩鲜明的、让人垂涎欲滴的美食图片,片间隙旁边都随机有些做这个菜的步骤,每个菜文字的布局不尽相同,却又显得格外和谐。
妻子翻开食谱,她说道:“吃个水煮土豆吧!亲爱的”,满脸的甜蜜和她的声音一起来到你的身边,并且还笑嘻嘻的补充道:“亲爱的,我给你读一下这水煮土豆的做法,我应该能做的好呢”,紧接着,她也没关心你有没有听,然后念到:“先把土豆削皮,然后切成块,接着放到锅里煮。”每一步都是具体的指令,就按部就班地执行下去。妻子整个做土豆的过程咱们暂且略去,半个钟后,你尝到了一款叫做“水煮土豆”的菜,泪水不停的在你眼睛里打转,你感动吃到了妻子做的菜,但更多的是本能反应,妻子做的菜,就是含泪也要吃完。
2.4.2.面向对象编程
第二天,你比往常提前几分钟回去,在回家的路上,街边落满了银杏叶子,叶子金黄相间,有些叶片干枯的时间久了,有点发红,有的叶片则还有些许绿,红红黄黄绿绿的,稀疏地铺满整个路边。今天没有多少风,这些叶片静静地卧在那里,偶尔有微分吹来,叶片就不情愿的动一下,像极了夏天的大黄躺在狗窝旁边的阴凉下面,有人走动,抬一下头胡乱的吠叫一声。
不出意外,刚开门,“你回来了呀,亲爱的,今天挺早的嘛!”照例的,她踮起穿着拖鞋的脚尖,亲了一下你的额头,她继续说道:“昨天的水煮土豆好吃吗?我今天,又学了新菜,做了你尝尝”。你(⊙o⊙)…了一声,说道:“亲爱的,不要太累,今天我们去餐厅吃,我们不是有自己的餐厅吗?今天我们就去那里吃”。
你们选了东南角的一个位置,这里可以俯瞰整个贯穿城市江河的水面,两边的树整齐的排列着,天边淡淡卷云马上就要被吹散,蔚蓝的天空倒影在湖面上,一时竟不知是水在天中,还是天在水中,要不是风吹过湖面泛起涟漪,让人不自觉迷离。
妻子轻轻的问到:“亲爱的,你今天能和我讲讲你是怎么经营这些餐厅吗?我想给我们将来的孩子讲讲,告诉他/她如何像爸爸学习”。
你看着妻子如小鹿般的眼睛,让人爱恋,轻轻的捏了她的脸,然后才慢慢解释道。
“经营一家餐厅是一个复杂的过程,你需要不仅仅是食谱,你还需要厨师、服务员、菜单等。每个角色都有自己的职责,厨师负责做饭,服务员负责上菜,菜单上有各种菜品。每个人(或者说每个对象)都有自己的属性(比如厨师的名字、菜品的价格)和方法(厨师会做饭,服务员会上菜)。面向对象编程就像是经营一家餐厅。你把问题分解成一个个对象,每个对象都有自己的属性和方法。比如,你可以有一个“厨师”对象,它有一个“做饭”的方法,还有一个“菜单”对象,它包含了所有菜品的信息。通过对象之间的互动,你就能高效地管理整个餐厅。”
两个故事都讲完了,观众却懵了。
突然笔者也懵了:“Do I have a wife?” or , “do I have a my girl?”
2.5…net框架介绍
2.5.1.序言
上一篇文章我们说了“面向对象编程和面向过程编程”的故事,今天我们继续上面的故事。
妻子听着你讲怎么经营餐厅,那双美丽的眼睛直勾勾的盯着你,最开始她的眼睛睁的没有那么大,随着你的讲解,那眼神就像班级里的一个上进的学生盯着老师一样,求学若渴。突然她意识到,眼前这个侃侃而谈的男人不是别人,正是自己心爱的老公,脸上不禁泛起红晕,这些红晕就像那万绿丛中的一朵小红花,装饰着那白皙的脸颊,没有任何突兀,只让人觉得相得益彰。此时,她看你的眼神已经开始在拉丝,修长而又匀称的双手,不自觉的撑在下巴,活脱脱的粉丝遇到Idol。你内心大受鼓舞,或者说的确切一点,虚荣心得到极大的满足,说道:“餐厅最重要的就是要有一个厨师,有了厨师才是一个餐厅开展其他事物的起点。但中国有句古话,巧妇难饮无米之炊,只有厨师还远远不够,你还得有砍排骨的刀,削萝卜丝的刀,切肉的刀,……,除了刀具,是不是还得有一口好锅,以及各式各样的调料品等等,所有这些东西都得一应俱全,这些东西你得给大厨全部备齐全喽……”,对话还在继续中。
2.5.2. .NET 框架是什么?
想象一下你是一位大厨,而你的厨房就是 .NET 框架。 .NET 框架为你提供好各种炊具、食材和食谱,一切都为你准备好了,你只需要大显身手就行。
.NET 框架是由微软开发的一种软件开发平台,旨在让开发人员轻松创建各种应用程序。它包括了一系列工具、库和运行时环境,帮助你开发、部署和运行应用程序。
2.5.3.为什么要使用 .NET 框架?
多语言支持
就像你的厨房里有中餐、西餐、日料等食谱,.NET 框架支持多种编程语言,如 C#、VB.NET、F# 等等。无论你偏好哪种语言,.NET 都能满足你的需求。你可以在同一个项目中使用多种语言进行开发,这种灵活性就像在同一个厨房里同时烹饪中餐和西餐,互不干扰,还能相辅相成。
强大的库
.NET 提供了一个庞大的类库,就像你的厨房里有各种高级调料和炊具,可以让你轻松实现各种功能,而不需要从零开始。这些类库涵盖了从基本的输入输出操作到复杂的图形用户界面,简直是开发人员的万用宝盒。
跨平台
NET Core支持跨平台,就像你可以在不同的厨房里做同样美味的菜肴。无论是 Windows、Linux 还是 macOS,你都可以使用 .NET Core 来开发应用程序。这样你就不用担心换了个操作系统就得重新学习一套新的开发工具了。
高性能和安全性
**.NET 框架经过优化,性能强劲,安全性也得到了很好的保障。**就像一个防火防盗的现代厨房,你可以安心烹饪,而不用担心厨房失火或食材被偷。
庞大的社区和丰富的资源
**作为一个成熟的开发平台,.NET 拥有庞大的社区和丰富的学习资源。**遇到问题时,总有热心的开发者和详细的文档来帮你解决问题,就像你做饭时总能找到菜谱和烹饪视频。
2.5.4. .NET 框架的组成部分
CLR(Common Language Runtime)
这个是 .NET 的心脏,就像你的厨房里的主厨。它负责管理程序的执行,包括内存管理、安全性、异常处理等。CLR 还负责将不同语言编写的代码编译成中间语言(IL),并在运行时将 IL 编译成机器码,从而保证了不同语言之间的互操作性。
FCL(Framework Class Library)
这是 .NET 的调料库,里面包含了各种预定义的类和方法,让你可以轻松实现常见的功能,如文件操作、数据访问、图形用户界面等。不管你是想读写文件、访问数据库、还是创建用户界面,FCL 都为你准备好了现成的工具。
ASP.NET
这是为你提供的烘焙区,专门用来制作 Web 应用程序和服务。你可以用它来创建动态网站、Web API 等等。ASP.NET 支持 MVC(Model-View-Controller)架构,帮助你将应用程序的逻辑、界面和数据分离,代码更加清晰易维护。
WPF(Windows Presentation Foundation)
这是你用来创建美观桌面应用的装饰台。WPF 提供了强大的 UI 框架,让你可以创建现代、流畅的桌面应用程序。 WPF 支持 XAML(Extensible Application Markup Language),你可以用它来定义用户界面的布局、样式等,让你的应用看起来更加专业。
WCF(Windows Communication Foundation)
这是你的通信工具箱,用来构建面向服务的应用程序。WCF 支持各种通信协议,让你可以在不同平台和系统之间轻松进行数据交换。不管是 HTTP、TCP 还是命名管道,WCF 都能帮你搞定,让你的应用程序可以与其他系统无缝对接。
Entity Framework
这是你的 ORM(对象关系映射)工具箱,用来简化数据库访问。Entity Framework 让你可以使用对象来操作数据库,而不需要写繁琐的 SQL 语句。通过 Code First 或 Database First 方法,你可以轻松定义数据模型和数据库结构,让数据操作变得更加直观。
2.5.5. .NET 框架的历史
.NET 框架的第一次亮相是在 2002 年,当时它还是一个小宝宝,版本是 1.0。当时的 .NET 主要用于 Windows 应用开发,虽然功能有限,但已经为未来的发展打下了坚实的基础。
随着时间的推移,.NET 框架逐渐成长,添加了更多功能,变得更加成熟和强大。例如,加入了 WPF 和 WCF,让开发桌面应用和服务变得更加轻松。每次更新都会带来性能优化和新功能,就像厨房里不断添置新的高科技炊具,让你的烹饪过程更加高效和愉快。
在 2016 年,.NET Core 出现了,这是 .NET 框架的进化版本,更加轻量、跨平台,并且开源。 .NET Core 让你可以在不同的操作系统上开发和运行应用程序,打破了平台限制,让开发变得更加灵活。
从.net 5 以后,微软统一了.NET Core 和 .NET Framework,直接叫.Net(没有core),让开发者只需学习一套工具和库,就能在不同平台上开发应用。
2.5.6.总结
.Net 现在就像一个万能的厨房,里面什么都有,你能想到的都有,但是你要做什么菜?能不能做出菜?做出的菜好不好吃,这就不是厨房的原因了,是考验每个厨师厨艺的时候,在.Net 的世界里,每个程序员都是厨师,你做的菜好吃吗?
2.6…NET 框架的架构与组件
2.6.1.序言故事
与妻子聊起餐厅的经营之道已经过去一个月了,具体内容请看“.net框架介绍”。
你们是在大学里认识的。
午后的阳光照在学校的花台上,花台里的小灌木风景树,被修建的整整齐齐,这种修剪,让人们想起了那些想彰显个性的小年轻的发型,通常他们会在头顶上方用推剪推出个像飞机跑道似得发型,两鬓的头发也被推平。这些小灌木风景树挺立在那里,觉得自己能吸引路人的眼光,就像那些小年轻觉得自己杀马特发型能吸引女孩子。
沿着花台旁边的广场往前走,会看到一片草地,草地在花台的尽头处,被另外一些小灌木围成一个圈,草地就在圈内,看起来长势很好。在小灌木与草地的交界处,时不时会看到一只壁虎爬出来或者爬进去。女孩在前面蹲下来了,看起来是在系鞋带。你继续往前走,来到她旁边,但接下来的发生的事,你要花费很长时间才能使自己的内心平静下来。女孩手里拿着壁虎,然后和小壁虎嘀咕着什么,“你要去那里呀?这么热的天气,怎么不躲在家里呀?……”女孩子又嘀咕了好一会,你注意到一个类似蚯蚓的东西正在地上蠕动,但是动的速度和评率比蚯蚓快多了,你定睛一看,这哪里是什么蚯蚓?这是那只小壁虎刚才为了自救断了的尾巴。女孩子自顾自和小壁虎说着话,根本没有理会地上那有点瘆人的会动的尾巴。
.NET 框架的架构可以分为多个层次,就像一个多层的蛋糕,每一层都有其独特的功能和职责。下面是各个主要部分的详细说明:
2.6.2.公共语言运行时(CLR)
CLR(Common Language Runtime)是 .NET 框架的核心,负责管理和执行 .NET 程序。它提供了内存管理、线程管理、垃圾回收、异常处理、安全性等关键服务。想象一下,CLR 就像是你的程序的保姆,确保程序安全、稳定地运行,并及时处理各种问题。
内存管理:CLR 自动管理内存分配和释放,使用垃圾回收机制(Garbage Collection)来回收不再使用的对象,防止内存泄漏。
线程管理:CLR 管理线程的创建和调度,支持多线程编程,使程序能够高效地执行并发任务。
异常处理:CLR 提供了结构化异常处理机制,帮助开发者捕获和处理运行时错误,确保程序的稳定性。
安全性:CLR 实现了代码访问安全性(Code Access Security)和验证(Verification),确保只有被授权的代码才能执行特定操作。
2.6.3.框架类库(FCL)
FCL(Framework Class Library)是 .NET 框架的标准库,包含了大量的可重用类、接口和数据类型,为开发者提供了丰富的功能支持。可以把 FCL 看作是工具箱,里面装满了各种开发工具,让编程变得更加便捷。
基础类库:提供基本的数据类型、集合、I/O 操作、文本处理等功能。
ADO.NET:用于数据访问和操作的组件,支持与各种数据库的连接和交互。
ASP.NET:用于构建动态 Web 应用程序的框架,包括 Web 窗体、MVC、Web API 等。
Windows 窗体(WinForms):用于构建桌面应用程序的组件库。
Windows Presentation Foundation(WPF):用于构建现代化桌面用户界面的框架,支持丰富的图形和多媒体功能。
Windows Communication Foundation(WCF):用于构建面向服务的应用程序和分布式系统的框架。
Entity Framework(EF):对象关系映射(ORM)框架,简化了数据访问层的开发。
2.6.4.应用程序域(App Domain)
应用程序域是 .NET 运行时环境中用于隔离应用程序的逻辑容器。每个应用程序域可以看作是一个独立的进程,提供了更高的隔离性和安全性。
隔离性:应用程序域之间相互隔离,一个域中的错误不会影响到其他域的运行。
跨域通信:可以通过 .NET 提供的跨域通信机制,在不同的应用程序域之间传递数据和调用方法。
2.6.5.公共类型系统(CTS):
CTS(Common Type System)定义了 .NET 中所有数据类型的规则和行为,确保不同编程语言之间的互操作性。CTS 可以看作是语言的共同基础,确保所有 .NET 语言编写的代码都能互相理解和协作。
2.6.6.公共语言规范(CLS)
CLS(Common Language Specification)是一组语言互操作性的规范,定义了所有 .NET 语言必须遵循的规则。CLS 确保了不同语言编写的代码可以无缝集成和互操作。
2.6.7.NET 框架的组件
除了上述核心架构部分,.NET 框架还包含了一些重要的组件,这些组件为开发者提供了强大的功能支持:
编译器(Compilers):.NET 框架支持多种编程语言,如 C#、VB.NET、F# 等。每种语言都有自己的编译器,将源代码编译为中间语言(IL),然后由 CLR 执行。
调试器(Debugger):.NET 提供了强大的调试工具,可以帮助开发者排查和修复代码中的错误。Visual Studio 是最常用的调试工具,支持断点、监视、堆栈跟踪等功能。
配置系统(Configuration System):.NET 框架提供了灵活的配置系统,允许开发者通过配置文件(如 app.config 和 web.config)来管理应用程序的设置和行为。
程序集(Assemblies):程序集是 .NET 程序的基本部署单元,包含了代码、资源和元数据。程序集可以是可执行文件(.exe)或库文件(.dll),支持版本控制和部署。
元数据(Metadata):元数据是关于代码的描述信息,包含了类型定义、方法签名、属性等信息。CLR 使用元数据来执行代码和实现反射功能。
2.7.C#语言介绍
C#(读作“C-Sharp”)是微软公司开发的一种现代、通用、面向对象的编程语言。它于2000年首次发布,并作为.NET框架的一部分进行推广。C#语言结合了C++的高性能和Visual Basic的简单易用,旨在提供一种既强大又易于学习的语言。
2.7.1.C#的历史背景
C#的开发始于1999年,由Anders Hejlsberg领导的团队负责。这一语言的设计初衷是为了支持软件组件的快速开发,并且具有跨平台兼容性。随着.NET Core和.NET 5/6的推出,C#进一步增强了跨平台的开发能力。
2.7.2.C#的主要特性
面向对象编程(OOP):C#完全支持面向对象编程,包括封装、继承和多态性。面向对象编程使得代码更具模块化和可维护性。
类型安全:C#是一种强类型语言,编译器在编译时进行类型检查,从而减少运行时错误。类型安全性确保了变量只能包含特定类型的数据,从而提高了代码的可靠性。
自动垃圾回收:C#中的垃圾回收器(Garbage Collector)自动管理内存,从而减少内存泄漏和管理复杂性。开发者不需要手动释放内存,这有助于防止内存泄漏和其他与内存管理相关的问题。
简洁且易读的语法:C#的语法设计简单且一致,使得代码易于编写和维护。开发者可以迅速上手并编写高效的代码。
跨平台支持:通过.NET Core和.NET 5/6,C#可以在Windows、Linux和macOS等多种平台上运行。跨平台支持使得开发者可以编写一次代码并在多个平台上运行。
丰富的类库:C#提供了丰富的类库(Class Library),涵盖了从数据访问、网络通信到图形界面开发等各个方面的功能,使得开发工作更加高效。
2.7.3.C#的基础语法
变量和数据类型:C#支持多种数据类型,包括整数、浮点数、字符、字符串和布尔值。
int number = 10;
double price = 99.99;
string name = "John";
bool isActive = true;
- 条件语句:C#使用if、else if和else语句进行条件判断。
if (number > 0) {
Console.WriteLine("Number is positive");
} else {
Console.WriteLine("Number is not positive");
}
- 循环:C#支持多种循环结构,包括for循环、while循环和foreach循环。
for (int i = 0; i < 10; i++) {
Console.WriteLine(i);
}
int j = 0;
while (j < 10) {
Console.WriteLine(j);
j++;
}
int[] numbers = { 1, 2, 3, 4, 5 };
foreach (int num in numbers) {
Console.WriteLine(num);
}
- 方法:方法是C#中代码的基本组织单位,用于执行特定的任务。
public void Greet() {
Console.WriteLine("Hello, World!");
}
public int Add(int a, int b) {
return a + b;
}
- 类和对象:C#是面向对象的编程语言,类是对象的蓝图。
public class Person {
public string Name { get; set; }
public int Age { get; set; }
public void Introduce() {
Console.WriteLine($"Hello, my name is {Name} and I am {Age} years old.");
}
}
Person person = new Person();
person.Name = "John";
person.Age = 30;
person.Introduce();
2.7.4.面向对象编程
C#支持所有面向对象编程的核心概念:
- 类和对象:
public class Person {
public string Name { get; set; }
public int Age { get; set; }
public void Introduce() {
Console.WriteLine($"Hello, my name is {Name} and I am {Age} years old.");
}
}
Person person = new Person();
person.Name = "John";
person.Age = 30;
person.Introduce();
- 继承:继承允许一个类从另一个类继承字段和方法,从而实现代码重用。
public class Employee : Person {
public string Company { get; set; }
public void Work() {
Console.WriteLine($"{Name} is working at {Company}.");
}
}
Employee employee = new Employee();
employee.Name = "John";
employee.Age = 30;
employee.Company = "ABC Corp";
employee.Work();
- 多态性:多态性允许使用基类引用调用子类的方法,从而实现灵活的代码设计。
public class Animal {
public virtual void Speak() {
Console.WriteLine("Animal is making a sound");
}
}
public class Dog : Animal {
public override void Speak() {
Console.WriteLine("Dog is barking");
}
}
Animal myDog = new Dog();
myDog.Speak(); // 输出: Dog is barking
- 封装:封装通过将数据和方法封装在类内部来保护数据,并提供公共接口进行访问。
public class BankAccount {
private decimal balance;
public void Deposit(decimal amount) {
if (amount > 0) {
balance += amount;
}
}
public void Withdraw(decimal amount) {
if (amount <= balance) {
balance -= amount;
}
}
public decimal GetBalance() {
return balance;
}
}
BankAccount account = new BankAccount();
account.Deposit(100);
account.Withdraw(50);
Console.WriteLine(account.GetBalance()); // 输出: 50
2.7.5.C#的高级特性
LINQ(语言集成查询):LINQ使得查询操作可以直接在C#语言中进行,无论是对集合还是数据库。
int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
var evenNumbers = from num in numbers
where num % 2 == 0
select num;
foreach (var num in evenNumbers) {
Console.WriteLine(num); // 输出: 2, 4, 6, 8, 10
}
异步编程:C#通过async和await关键字提供了强大的异步编程支持。
public async Task<string> GetDataAsync() {
using (HttpClient client = new HttpClient()) {
string data = await client.GetStringAsync("http://example.com");
return data;
}
}
public async void ShowData() {
string data = await GetDataAsync();
Console.WriteLine(data);
}
属性:属性是对类中字段的封装,提供了对字段的访问控制。
public class Person {
private string name;
public string Name {
get { return name; }
set { name = value; }
}
}
2.8.创建第一个console应用程序
2.8.1.安装Visual Studio
(1)下载Visual Studio:访问Visual Studio官网,下载免费的社区版(Visual Studio Community)。有渠道的同学,直接上一个企业版,社区版有些功能被阉割了
(2)安装Visual Studio:运行下载的安装程序,选择安装工作负载时,确保勾选“.NET 桌面开发”选项,这将安装C#和必要的工具来创建控制台应用程序。
提示:如果你还想学asp.net core, 那就将asp.net core 的工作负载也勾选上,如果你还想玩unity3d,那把这个负载也勾选上。
2.8.2.启动Visual Studio
打开Visual Studio:安装完成后,启动Visual Studio 2022。
登录或创建账户:如果你没有微软账户,可以创建一个。如果有,直接登录。也可以直接跳过。登录账户有一个好处,直接在visual studio 2022中就可以将代码一键推到仓库,很方便。
2.8.3.创建新的项目
开始页面:在Visual Studio启动页面上,点击创建新项目。
选择项目模板:在“创建新项目”窗口中,搜索并选择控制台应用模板。确保选择C#语言。选择模板后,点击下一步。第一个模版是基于.net core 的框架,而第二个是基于.net framework框架的。
注意:项目模版有多种多样,可以根据需要选择,如有疑惑请点击链接:不同类型的C#应用程序。
设置项目名称和位置:输入项目的名称,例如HelloWorld。选择项目保存的位置。
解决方案名称:通常与项目名称相同。点击创建按钮。
2.8.4.编写代码
Visual Studio会创建一个包含基础模板代码的项目。你会看到一个名为Program.cs的文件,打开它,你会看到类似如下的代码:
这种写法没有采用顶级语句:
using System;
namespace HelloWorld
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello, World!");
}
}
}
当然,你也可以勾选顶级语句,main函数都可以省略不写
Console.WriteLine("Hello, World!");
2.8.5.运行程序
保存代码:确保保存你所编辑的代码文件。
运行程序:点击顶部菜单栏的绿色箭头按钮(或按Ctrl+F5),程序会编译并运行。
你会看到一个控制台窗口打开,并显示“Hello, World!”。
2.8.6.修改代码
你可以修改Main方法中的代码以输出其他内容,例如:
using System;
namespace HelloWorld
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello, [Your Name]!");
}
}
}
将[Your Name]替换为你的名字,然后再次运行程序,查看输出结果。
2.8.7.详细步骤和补充说明
2.8.7.1.使用命名空间
在C#中,命名空间用于组织代码。namespace HelloWorld表示这个类属于HelloWorld命名空间。命名空间帮助防止类名冲突,并使代码更加结构化。
2.8.7.2.类和方法
类:class Program定义了一个名为Program的类,类是C#中的基本构造块。
方法:static void Main(string[] args)是程序的入口点,当程序启动时,Main方法是第一个被调用的方法。
2.8.8.调试
Visual Studio提供了强大的调试工具。你可以在代码行号左侧点击设置断点,然后运行程序时,它会在断点处暂停,让你检查变量值和程序执行流程。
2.8.9.快捷键
使用Visual Studio的代码格式化工具(快捷键Ctrl+K, Ctrl+D)可以使代码更加整洁和易读。
注释代码:ctrl + k,ctrl + c 取消注释ctrl + k,ctrl + u;
对齐:ctrl + k,ctrl + f
我们经常写console.writeline(“”),我们不用每个字母都打,只需要输入cw,按下tab键,整条语句就写好了
2.8.10.学习资源
利用Visual Studio内置的帮助文档和在线资源,学习更多C#编程知识。
Microsoft Docs是一个很好的资源:点击「链接」
当我们遇到不会代码,将鼠标放到visual studio的方法上面,按下f1键,就可以打开在线帮助文档,不要太爽。
2.9.C#从入门到精通学习线路
C#是一个庞杂的编程系统,开发者可以根据不同的框架和工具,开发各种各样的应用。C#一直被人诟病的就是,新的特性一出来,老版本的各种框架就不再支持,增加开发者的学习成本。大家想一下,过去十年最火的游戏—英雄联盟,从第一个赛季到现在,经过多少次版本变化,增加了多少新英雄,天赋和符文变了多少次,玩家和观众换了多少批,虽然不能否认这个游戏正在走下坡路,但他仍然是过去十年最优秀的3d游戏。作为程序开发者,你想掌握一门技能,然后。靠着这个技能一直吃混吃到老,在各种ai大模型流行的今天,你觉得这现实吗?
所以我一直推荐,当有新的版本出来的时候,有新的特性发布的时候,我们就用最新的版本、最新的特性,这会给我们开发者带来了极大的便利,提高我们代码的开发质量。我们不得不承认一个点,设计微软语法或者说框架的这批人,他肯定是比绝大多数人对编程都更优秀的,对未来的编程趋势更加理解。
2.9.1.框架介绍
我们首先还是对c sharp的各种框架做一个简单的介绍。
.net framework是一个微软2000年左右发布的第一个版本框架,这个版本呢,相对比较老,最后的版本大概是.net framework 4.8点,微软官方已经停止对.net framework框架的继续支持。在该框架上可以开发各种各样的程序,比如用wpf和winform开发桌面应用软件,用asp. net开发网页,等等。 .net framework 存在一个问题,它仅支持在windows的平台开发,它不支持跨平台。在当今社会开源流行的这么一个情况下,其实.net framework并不能很好地满足各种开发需求,应该算是一个过时的产品了。当然,如果你所在的公司正在使用.net framework开发的历史遗留软件,运行很稳定,那你千万不要想着重新构建代码,这是一个很不明智的做法,我推荐你继续在老版本上维护就可以了,即使是屎山代码。
.net core是2015年微软推出的一个跨平台、高性能、开源框架。它不仅支持传统的windows开发,同时呢又支持Linux和Mac OS系统的开发,甚至支持安卓手机和苹果手机的软件开发。
再后来,.net core变成.net了,应该是从.net5开始的,为什么呢?微软想统一.net Framework 和 .net core。
还有很多小伙伴会迷惑,asp.net 和asp.net core,两者都是用于web开发的,不同的是前面是一个相对比较老的版本,而后面这个则是面向现代网页开发的,它增加了许多新的、面向现代网页开发的一些特性。
2.9.2.基础知识
变量、数据类型和操作符:理解变量声明、数据类型、操作符的使用。
条件语句:掌握if、else if、else和switch语句。
面向对象编程(OOP):包括类、对象、继承、多态、封装和抽象。
异常处理:使用try、catch、finally和throw来处理异常。
2.9.3.高级概念
事件、委托和Lambda表达式:处理事件,使用委托和Lambda表达式。
泛型:定义和使用泛型类和方法。
多线程:创建和管理线程,处理线程同步。
异步编程:使用async和await关键字实现异步操作。
集合:理解和使用各种集合类型如List、Dictionary等。
文件处理:读写文件,文件流操作。
并行编程:使用并行编程库提高应用程序性能。
AutoMapper:对象到对象映射。
ADO.NET:数据访问技术,连接数据库、执行查询。
LINQ:使用LINQ进行数据查询和操作。
2.9.4. Web开发
ASP.NET MVC:MVC模式、路由、控制器、视图、模型。
ASP.NET Web API:RESTful API、HTTP请求处理、路由、模型绑定等
Blazor:C#构建交互式Web UI。
前端框架:Angular或React。
2.9.5.数据库基础知识
SQL和关系型数据库概念:表、列、约束、行、键、索引和关系。
CRUD操作:创建、读取、更新、删除。嗯
连接、事务、存储函数、存储过程、触发器等。
2.9.6.Entity Framework
Entity Framework是一个对象关系映射(ORM)框架,实现实体类到数据库的映射。关键概念包括:
建模:EF基于POCO实体创建实体数据模型(EDM)。
查询:使用LINQ或原生SQL查询数据库。
更改跟踪:追踪实体实例的变化。
保存:执行数据库的插入、更新、删除操作。
并发:默认使用乐观并发控制。
事务:自动管理事务,可自定义。
缓存:提供一级缓存。
内置约定和配置:通过数据注解属性或流畅API配置模型。
2.9.7.性能调优和优化
使用Visual Studio诊断工具,帮助诊断CPU使用率、内存使用率、GPU使用率等。
也可以使用JetBrains系列软件调优。JetBrains dotTrace:性能分析器,检测性能瓶颈。JetBrains dotMemory:内存分析器,识别内存泄漏和优化内存使用。
Windows事件跟踪(ETW):ETW是Windows提供的高速跟踪设施,用于跟踪.NET应用程序、系统事件和内核事件。将来你肯定会遇到软件崩了,有时候是在找不到问题,这时候不放试试windows事件跟踪器。
2.9.8.其他
估计小伙伴会提到Azure ,或者单元测试,亦或者部署。博主这些不会,得小伙伴们找找其他教程看一看
2.10.C#开发者的最佳实践
在编程领域经常听到的一句话:最佳实践。当你听到这短短的4个字的时候,你一定想知道它到底是什么意思?今天我想深入聊一聊这个话题,并通过具体的例子给出了C#程序开发的最佳实践。
我们先来讲一个小故事:为什么追女孩子总是从加微信开始?为什么不是要电话,不是写情书呢?你当然可以要电话,但你要电话时,旁人会以为你是销售。你也可以写情书,但每个字都要自己写,麻烦,还不知道女孩到底看还是没看你的情书。所以现代社会追一个女孩子基本都是从要微信开始,因为微信交流方便,可以看朋友圈、点赞,互动性很好,慢慢的你们就有可能有交集了。当然,你可能有其他方式搭讪,但要微信绝对是多数人的首选,这就是搭讪的最佳实践。
最佳实践通常是指在某个特定领域或行业中,经过广泛验证并被认为是最有效、最可靠的方法或策略。那在编程领域的最佳实践呢,就是程序员总结出来的最简洁、最有效的开发方式。
那既然是最佳实践,肯定会带有笔者的个人情感,我认为的好方法,其他人从另外一个维度来看,却糟糕透了,因此最佳实践并不是唯一的。我总结的C#开发者的最佳实践,是我多年的编程经验总结的一套编程方法论,其中很多经验是踩了很多坑才悟出来的,现在我开发始终坚持这些原则,也希望这些方法对您编程有帮助。
2.10.1.最佳实践1:命名要合理
在新项目中,默认项目名称和解决方案名称通常是“consoleApplication ”,这是糟糕的命名方式,因为无法理解它们的实际功能,当你有多个项目时,很可能不知道某个项目的具体功能。应该为项目命名一个具有实际意义的名称。例如,将项目命名为“consoleMathMethod”,这个名称告诉我们它是应用程序的控制台部分,并且与数学方法有关。
另外一个就是变量名,比如温度,你可以用汉语拼音“wendu”,也可以用汉语拼音首字母“WD",这些都可以。但是,这很容易出岔子,比如一个哥们想给”社保“起个名字,就叫”shebao“,但这哥们图省事,简写成”sb",这哥们不会被社保局的人追着打吧,毕竟“shebao”和“sb”之间还是有区别的! 还是用回英文吧:Temperature,也可以用Temper。
2.10.2.最佳实践2:每个文件仅包含一个类
在这个示例中,如果要添加多个类,应为每个类创建一个单独的文件。例如,创建一个名为“Person”的类文件和一个名为“Guest”的类文件,我们把它放到两个cs文件中。这样可以通过查看文件结构,轻松找到每个类的位置。
当然了,你非要犟,就是要把这两个类写在一个文件中,就像下面这个例子:文件的名字是MyClass.cs,但点击进去时,却发现有两个其他的类,Person类和Guest类,这个习惯很不好,甚至可以说恶劣,别人接手你的代码肯定爆粗口,有时候你自己找不到相关类的文件时,也会忍不住给自己一个大嘴巴子。
//MyClass.cs
public class Person
{
public string Name { get; set; }
// ... 其他成员 ...
}
public class Guest
{
public Person Host { get; set; }
public DateTime ArrivalDate { get; set; }
// ... 其他成员 ...
}
2.10.3.最佳实践3:使用属性而不是公共变量
属性允许在获取和设置值时执行额外的逻辑,例如验证数据或引发事件。这确保了数据的一致性和完整性。例如,使用属性可以确保在设置负值之前进行验证。
private int age;
public int Age
{
get { return age; }
set
{
if (value >= 0)
{
age = value;
}
else
{
throw new ArgumentOutOfRangeException("Age cannot be negative.");
}
}
}
相比之下,公共字段无法实现这种控制:
public int age;
2.10.4.最佳实践4:方法应只做一件事
方法应只做一件事,且只做一件事。将复杂的方法拆分为多个简单的方法,使每个方法职责单一。这不仅有助于提高代码的可读性,也更容易进行调试和维护。遵循单一职责原则,有助于确保代码模块的独立性和复用性。下面的例子,每个方法只关注其自身的功能,简化了代码的逻辑。
public void ProcessOrder()
{
ValidateOrder();
CalculateTotal();
SaveOrder();
}
private void ValidateOrder() { /* 验证订单逻辑 */ }
private void CalculateTotal() { /* 计算总价逻辑 */ }
private void SaveOrder() { /* 保存订单逻辑 */ }
2.10.5.最佳实践5:仅在必要时使用公共修饰符
默认情况下,应将方法和属性设为私有(private),只有在需要从类外部访问时,才将其设为公共(public)。这样可以减少不必要的访问权限,增强类的封装性和安全性。例如,我们将age 设为私有字段,类外的成员就无法直接访问了,如果类外的成员想访问,可以通过公共方法GetAge()来访问。
public class Person
{
public string FirstName { get; set; } // 公共属性
public string LastName { get; set; } // 公共属性
private int age; // 私有字段
public int GetAge() // 公共方法,用于访问私有字段
{
return age;
}
}
这种做法有助于确保只有需要公开的方法和属性才会被暴露,其他实现细节保持私有。
2.10.6.最佳实践6:保持代码简单
代码应尽量保持简单,不要编写过于复杂的代码。如果发现代码非常复杂,应暂停并考虑是否有更简单的方法实现同样的功能。简化代码不仅有助于提高可读性,也更容易进行调试。例如,使用foreach循环而不是复杂的for循环:
// 简单的foreach循环
foreach (var person in people)
{
Console.WriteLine($"Hello, {person.FirstName} {person.LastName}");
}
// 复杂的for循环,这里就是举例子,实际中应该也没人这么写
for (int i = 0; i < people.Count; i++)
{
Console.WriteLine($"Hello, {people[i].FirstName} {people[i].LastName}");
}
2.10.7.最佳实践7:保持一致性
什么是保持一致性,大家都军训过,稍息出的是左脚,敬礼是抬右手,但有个二货,他就是反着来,稍息出右脚,敬礼抬左手。我已经能想象教官喷他狗血淋头的样子了。编程也是这样,做一件事时,保持一样的风格
变量声明:始终使用相同的方式声明变量。例如,在C#中,可以选择使用明确的类型声明或使用var关键字,但无论选择哪种方式,都应在整个项目中保持一致。
// 不一致的变量声明
int age = 25;
var name = "Alice";
// 一致的变量声明
int age = 25;
string name = "Alice";
命名约定:为类、方法、变量等制定并遵循一致的命名约定。例如,类名使用Pascal命名法,方法名使用Camel命名法。
// Pascal命名法用于类名
public class UserManager {}
// Camel命名法用于方法名
public void getUserDetails() {}
代码格式:保持一致的代码格式,包括缩进、空格和换行。使用自动格式化工具可以帮助维持一致的代码格式。
// 不一致的代码格式
public void DoSomething(){
if(true){Console.WriteLine("Hello");}
}
// 一致的代码格式
public void DoSomething()
{
if (true)
{
Console.WriteLine("Hello");
}
}
注释风格:保持一致的注释风格和格式。确保注释清晰且简洁,并与代码保持同步。
// 单行注释
// 计算总和
int sum = a + b;
// 多行注释
/*
计算总和
变量a和b是输入参数
*/
int sum = a + b;
2.10.8.最佳实践8:在if语句中使用大括号
在if语句中始终使用大括号,即使条件语句只有一行代码。这样做有以下几个好处:
(1)防止错误:在只有一行代码的if语句中,如果未来需要增加新代码,容易因为忘记添加大括号而导致错误。始终使用大括号可以避免这种情况。
(2)提高可读性:大括号使代码结构更加清晰,容易辨认条件语句的范围,提高代码的可读性。
(3)一致性:无论条件语句的代码行数,始终使用大括号可以保持代码风格的一致性。以下是一个示例:
2.10.9.最佳实践9:使用字符串插值
在连接字符串时,使用字符串插值($“”)而不是使用加号(+)连接。这不仅更易读,还能提高代码的效率。
为什么使用字符串插值?(1)可读性:字符串插值使得代码更清晰、更易读。变量和文本混合在一起,直观易懂。(2)效率:字符串插值在编译时进行优化,通常比使用加号连接字符串更高效。(3)简洁性:使用字符串插值可以减少代码量,使代码更加简洁。我们来看下面这个例子:
//使用加号连接字符串
string name = "Alice";
int age = 30;
string message = "Name: " + name + ", Age: " + age;
Console.WriteLine(message);
//使用字符串插值
string name = "Alice";
int age = 30;
string message = $"Name: {name}, Age: {age}";
Console.WriteLine(message)
2.10.10.最佳实践10:避免使用全局变量
避免使用全局变量,而应通过参数传递或其他方式共享数据。这样可以减少内存占用,并提高代码的模块化和可维护性。
为什么避免使用全局变量?
(1)减少耦合:全局变量会导致代码之间的高耦合,难以维护和扩展。
(2)提高模块化:通过参数传递等方式共享数据,可以使代码更模块化,便于测试和复用。
(3)降低错误风险:全局变量的值可以在程序的任何地方被修改,容易引起难以发现的错误。
(4)优化内存使用:全局变量会一直占用内存,直到程序结束,局部变量和参数传递可以有效管理内存使用。
我们来看一个例子:
//使用全局变量(不推荐)
public class Program
{
public static int globalCounter = 0;
public static void Main()
{
IncrementCounter();
Console.WriteLine(globalCounter);
}
public static void IncrementCounter()
{
globalCounter++;
}
}
//使用参数传递(推荐)
public class Program
{
public static void Main()
{
int counter = 0;
counter = IncrementCounter(counter);
Console.WriteLine(counter);
}
public static int IncrementCounter(int counter)
{
return ++counter;
}
}
2.10.11.最佳实践11:注释和文档
为什么注释和文档很重要?
(1)提高可维护性:好的注释和文档能帮助其他开发人员快速理解代码,尤其是在原始开发人员不在时。
(2)减少沟通成本:详细的文档和注释可以减少开发团队间的沟通成本,提升工作效率。
(3)明确代码意图:通过注释解释为什么做某事,可以让代码意图更加明确,避免误解。
(4)便于长期维护:随着时间推移,详细的注释和文档可以帮助维护人员更好地理解和修改代码。
注释的最佳实践,我列出了一下几点:
(1)解释“为什么”而不是“如何”:注释应解释代码的目的和原因,而不是描述代码的工作原理。
// 坏的注释
// Increment the counter by 1
counter++;
// 好的注释
// 增加计数器,因为我们需要跟踪操作次数
counter++;
(2)简洁和相关:保持注释简洁并且与代码相关,避免冗长和不必要的注释。
// 坏的注释
//这个函数的功能是求解两数之和,并且将和作为返回值
public int Add(int a, int b)
{
return a + b;
}
// 好的注释
// 这个函数用于处理交易时的金额合并
public int Add(int a, int b)
{
return a + b;
}
文档的最佳实践
(1)设置说明:文档应包括如何设置项目的详细说明,确保新开发人员能快速上手。
a. 克隆仓库:git clone https://github.com/example/project.git
b. 安装依赖:npm install
c. 运行项目:npm start
(2)使用详细信息:包括项目的使用说明和示例,帮助用户了解如何使用项目。
例如,要运行应用程序,请执行以下命令:
npm start
第二步:
……
第三步
……
2.10.12.最佳实践12:不要信任用户输入
永远不要相信互联网,因为你不知道对方是魔鬼还是阎王。用户输入的数据可能会导致应用程序崩溃,甚至引发安全漏洞,一定要对用户输入进行严格的验证和处理。
为什么不信任用户输入?
(1)安全性:未验证的用户输入可能导致SQL注入、XSS攻击等安全漏洞。(2)稳定性:未验证的用户输入可能导致应用程序崩溃或产生意外行为。(3)数据完整性:未验证的用户输入可能导致数据库中的数据不一致或错误。
2.10.13.最佳实践13:在编写代码之前进行计划
为什么在编写代码之前进行计划?
(1)提高效率:提前计划可以减少返工和修复错误的时间,从而提高开发效率。
(2)减少错误:详细的计划和设计有助于识别潜在的问题,并在编码之前解决这些问题。
(3)明确目标:有了明确的计划,开发人员可以更清楚地知道他们需要实现什么,以及如何实现。
(4)团队协作:计划和设计文档可以作为团队成员之间的沟通工具,确保所有人对项目目标和实现方法有一致的理解。
如何进行有效的计划和设计?
我们以一个用户登录系统为例,来做详细说明:
(1)需求分析:在编写代码之前,首先需要进行需求分析,明确项目的目标和要求。以用户登录系统需求分析为例:
- 用户可以使用电子邮件和密码登录。
- 系统需要验证用户的电子邮件和密码。
- 用户成功登录后,系统应显示欢迎页面。
(2)系统设计:根据需求分析,设计系统的整体架构和组件,包括数据库设计、模块划分和接口设计。
(a)数据库设计
- 用户表(Users)
- 用户ID(UserID,主键)
- 电子邮件(Email)
- 密码(Password)
(b)模块划分
- 用户接口模块(User Interface Module)
- 用户验证模块(User Authentication Module)
- 数据库访问模块(Database Access Module)
(c)接口设计
- 登录接口(Login API)
- 输入:电子邮件、密码
- 输出:登录成功或失败信息
(3)流程图和用例图
使用流程图和用例图等工具,直观地展示系统的工作流程和用户交互。登录流程图如下图所示:
(4)编写伪代码或原型
在开始实际编码之前,编写伪代码或开发原型,以验证设计的可行性和合理性。伪代码如下:
Function Login(email, password)
If ValidateEmail(email) And ValidatePassword(password) Then
If AuthenticateUser(email, password) Then
Display "Welcome"
Else
Display "Invalid email or password"
End If
Else
Display "Invalid input format"
End If
End Function
2.10.14.最佳实践14:不要过度设计代码
你们肯定遇到过这样的人,床垫不整齐一点,如果不拉平整,那一晚上他都睡不着觉,和这些强迫症患者相处,很累。程序员中也有类似的人,比如前人留下的代码质量很烂,但能稳定运行,他就是要重构,重新设计框架,他自己那一部分改动就算了,很有可能会牵扯到你负责那个模块,你也得跟着折腾。
很多人做项目时,代码都写了好多,又觉得之前的设计框架不太合理,又想重新弄,想设计一个更完美的框架。其实,哪有什么完美的框架,一个项目做完,肯定有各种瑕疵,我觉得能按时交付就可以了,至于本项目某些地方设计不足,我们在下一个项目在完善就可以了。我们来举个例子:
我们要设计两个篮子:一个篮子装水果,一个篮子装蔬菜。于是我们就购买各种材料,编制了两个篮子。但是,使用者并没有完全按照这个设计去用者两个篮子,装蔬菜的篮子里装了水果,装水果的篮子也装了蔬菜,这或许就是不完美就是完美吧。
推进一个项目时也应该有这种心态:小步前进,快速迭代。遇到非致命问题时,可以先摆一摆,计划也跟着实际进度不断调整。
3.C#基础知识
3.1.C# 程序的基本结构
3.1.1.实例
讲C#程序中基本结构,我们用一个实际的例子。
using System; // 命名空间声明
namespace HelloWorldApp // 命名空间
{
class Program // 类
{
// Main方法 - 程序入口点
static void Main(string[] args)
{
// 输出信息到控制台
Console.WriteLine("Hello, World!");
}
}
}
3.1.2.命名空间 (Namespace)
什么是命名空间?
我们先说一下命名空间的作用:分门别类,方便查找和使用。
在家里,我们有不同的房间,有厨房、卫生间、卧室、客厅、书房等,我们会把菜刀、砧板、碗筷等炊具放到厨房,同样我们会把床、衣柜、梳妆台等放到卧室,但应该没有人会把马桶刷放到厨房吧,也不会把盛汤的碗丢到枕头旁边吧!我们这样做的目的就是分门别类,防止不同的东西混淆和冲突。这样做的好处就是,我想尿尿,我知道我要去找卫生间,肚子饿的时候我知道去厨房找吃的。
同样的,在编程中命名空间就像厨房、卫生间、客厅一样,它把具有相同属性的类、方法、成员变量等放在同一个命名空间之下,方便我们在后续需要使用这些元素时,我知道去哪个命名空间中找哪个类、哪个方法。
我们举一个通俗的例子:
假设你有三个箱子,每个箱子里放着不同类型的玩具:
第一个箱子:玩具汽车
第二个箱子:积木
第三个箱子:洋娃娃
每个箱子就像一个命名空间,它们帮助你把不同类型的玩具组织在一起,这样你就不会把汽车和积木混在一起。
// 汽车的命名空间
namespace Cars
{
class ToyCar
{
public void Drive()
{
Console.WriteLine("The car is driving.");
}
}
}
// 积木的命名空间
namespace Blocks
{
class BuildingBlock
{
public void Stack()
{
Console.WriteLine("The blocks are stacked.");
}
}
}
// 洋娃娃的命名空间
namespace Dolls
{
class Doll
{
public void Play()
{
Console.WriteLine("The doll is being played with.");
}
}
}
class Program
{
static void Main(string[] args)
{
// 使用命名空间中的类
Cars.ToyCar car = new Cars.ToyCar();
car.Drive();
Blocks.BuildingBlock block = new Blocks.BuildingBlock();
block.Stack();
Dolls.Doll doll = new Dolls.Doll();
doll.Play();
}
}
在 Main 方法中,我们使用了三个命名空间中的类,每个类都执行了它们各自的功能。这就像你打开不同的箱子,取出里面的玩具来玩,每个玩具都有自己的特性和功能。
3.1.3.命名空间声明 (Namespace declaration)
如果每次使用命名空间中的类都如下:
Cars.ToyCar car = new Cars.ToyCar();
Blocks.BuildingBlock block = new Blocks.BuildingBlock();
Dolls.Doll doll = new Dolls.Doll();
这样太累了,我们的命名空间就没有意义了,因此,我们需要对命名空间进行引用,怎么引用呢?我们来看下面这个例子:
//引用命名空间
using Cars;
using Blocks;
using Dolls;
//省略命名空间的名字
ToyCar car = new Cars.ToyCar();
BuildingBlock block = new Blocks.BuildingBlock();
Doll doll = new Dolls.Doll();
上面这个例子,在使用命名空间中的类时,因为我们已经声明过命名空间,实例化类时就可以不写命名空间。
3.1.4.类 (Class)
class Program:定义一个名为 Program 的类,我们一般都把主函数放到Program类里面,当然放到其他类中也没有问题
static void Main(string[] args):Program类中的一个静态方法 ,这是 C# 程序的入口点。每个 C# 控制台应用程序都必须有一个 Main 方法。static 关键字表示这个方法是属于类而不是类的实例的。void 表示这个方法没有返回值。
注意:即使是新的语法使用顶级语句,我们看不到main方法,其实并不是main方法不存在了,只不过是编译器给我们添加了一个语法糖,当反编译时,仍然有main方法。
输出信息 (Console Output):Console.WriteLine(“Hello, World!”),调用 Console 类的 WriteLine 方法,将 “Hello, World!” 输出到控制台。
3.1.5.补充内容
在实际的 C# 程序中,还可能包括以下内容:
注释 (Comments)
// 这是单行注释
/*
这是多行注释
*/
成员变量 (Member Variables)和成员方法
class Example
{
int number; // 成员变量
void DisplayMessage()//成员方法
{
Console.WriteLine("This is a member method.");
}
}
3.2.Console 类的方法和属性
如果你学C#,肯定会看到下面的界面,控制台打印“hello,world",但是很少有人具体来讲Console类到底是什么?本博主呢闲了蛋疼,我就想来捣鼓一下这个类,看看它有什么神秘的地方。
3.2.1.什么是 Console 类?
我们先明确Console类的作用:处理控制台窗口的输入和输出
Console类位于System命名空间中,是一个静态类,用于表示控制台应用程序的标准输入、输出和错误流。由于Console类是静态的,所以我们不需要创建它的实例就可以访问其所有成员。
3.2.2.Console 类的属性
在visual studio中,打出 “Console.WriteLine(“Hello,world”);”这个语句,鼠标光标放到“Console”这个单词上,然后按F12,我们就能看到Console类的定义了,你鼠标往下滑,估计很多新手小伙伴就吓到了,这么一个类,竟然有1000行代码,妈耶,劝退,哈哈。没事啊,听我娓娓道来。
我们不需要关注所有的属性,没必要,这有点像美国哪个州选州长、总统,咱们吃瓜就行,不需要去关注到他有多少选票。
我们来看几个关键的属性
Console.WriteLine("Hello,world");
//Title:获取或设置控制台标题栏中显示的标题。最大长度为24500个字符。
Console.Title = "My Console App";
//**BackgroundColor**:获取或设置控制台的背景颜色。
Console.BackgroundColor = ConsoleColor.Red;
//**ForegroundColor**:获取或设置控制台的前景色,即每个字符显示的颜色。
Console.ForegroundColor = ConsoleColor.Yellow;
//**CursorSize**:获取或设置光标在字符单元格中的高度,以百分比表示。值范围为1到100。
Console.CursorSize = 50;
运行结果如下,是不是和系统默认的颜色有改变了,嗯,还挺有意思。
3.2.3.Console 类的方法
我们也拎出几个关键点来说,每个方法的功能都在代码上面写有注释,大家可以阅读。大家可以打断点来一条一条执行下面代码,很有意思。
//**Clear()**:清除控制台缓冲区及相应的显示窗口信息。
Console.Clear();
//**Beep()**:通过控制台扬声器播放哔声。
Console.Beep(500,5000);//500hz,响5秒
//**ResetColor()**:将前景色和背景色设置为默认值。
Console.ResetColor();
//**Write(string)** 和 **WriteLine(string)**:
//- `Write`方法用于将指定的字符串写入标准输出流,不换行。
//- `WriteLine`方法用于将指定的字符串写入标准输出流,并在输出后移动到下一行。
Console.Write("Hello ");
Console.WriteLine("World!");
//**Read()**:
//- 从键盘读取一个字符并返回其ASCII值,返回类型为`int`。
int asciiValue = Console.Read();
//**ReadLine()**:
//- 从键盘读取一行字符串,并返回该字符串。
string input = Console.ReadLine();
//**ReadKey()**:
//- 从键盘读取一个字符,并返回包含该字符信息的`ConsoleKeyInfo`对象。
ConsoleKeyInfo keyInfo = Console.ReadKey();
以下示例展示了如何使用Write和WriteLine方法:
using System;
namespace MyFirstProject
{
internal class Program
{
static void Main(string[] args)
{
// WriteLine 方法打印值并移动到下一行
Console.WriteLine("Hello");
// Write 方法打印值并留在同一行
Console.Write("sunny老师 ");
Console.Write("Bye bye ");
// WriteLine 方法打印值并移动到下一行
Console.WriteLine("Welcome");
// Write 方法打印值并留在同一行
Console.Write("学英语,学C#系列教程 ");
//打印下一个输入,同时退出Console
Console.ReadKey();
}
}
}
输出结果:
Hello
sunny老师 Bye bye Welcome
学英语,学C#系列教程
Console类就讲完了,小伙伴你学会了吗?
本来下一期打算讲:C#的数据类型,当我惊奇的发现已经写过一篇这样的文了。
那我们就往下讲:“C#的字面值"。字面值(Literals)就是写在代码中的固定值。它们直接表示数据,而不是通过变量或计算得到的结果。字面值是一种简单、直接的方式来表达具体的数值、字符、字符串等。
3.3.C#中的数据类型
3.3.1.C# 中数据类型有哪些?
C# 中的数据类型指定变量可以存储的数据类型,例如整数、浮点数、布尔值、字符、字符串等。下图显示了 C# 中可用的不同类型的数据类型。C# 语言中有 3 种类型的数据类型可用。
3.3.2.值类型
3.3.2.1.无符号Byte
它是一种 .NET 数据类型,用于表示 8 位无符号整数。byte 仅表示正值。由于它只存储正数,因此它可以存储的最小值为 0,可以存储的最大值为 255。下面我们定义一个byte数据,打印的时候是65,当我们看它的数据大小的时候是1个Byte大小。
当我们赋值超过范围时,编译器会报错:
3.3.2.2.有符号Byte
如果要同时存储正值和负值,则需要使用有符号字节数据类型,即sbyte。sbyte 数据类型表示一个 8 位有符号整数。最大值是128,最小值是-128。
当我们赋值超过范围时,编译器会报错:
3.3.2.3.ASCII 码
下面这张表是标准的ASCII码表,我们可以看到大写字母A用整数65表示, 小写字母a用97表示。
3.3.2.4.char
char 是一种 2 字节长度的数据类型,它是一种有符号数据类型,这意味着它只能存储正数。能表示的最小数据是0,最大的数据是65535,我们在代码上看看它是怎么运行的:
前面我们提到ASCII 码可以用整数表示,我们来做一个强制转换,看一下是什么结果。85表示ASCII 码中的大写U
char数据类型需要 2 个字节,我们明明使用1 个字节的数据就能表示这些字母,那么byte 和 char 在做同样的事情,为什么我们需要 char 数据类型来占用额外的 1 字节内存?byte只能表示 256 个字符,如果我们想存储一些额外的符号,如中文或一些不属于 ASCII 字符的特殊符号,显然一个字节是不够的,所以我们需要两个字节。
3.3.2.5.int
.NET Framework 提供了三种int数据类型:
16 位有符号数值: Int16
32 位有符号数值: Int32
64 位有符号数值: Int64
由于上述数据类型是有符号数据类型,因此它们可以同时存储正数和负数。根据数据类型,它们可以容纳的大小会有所不同。
① Int16
因为它是 16 位的,所以它将存储 216个数字,即 65536。因此,正数将从 0 到 32,767 开始,负数将从 -1 到 -32,768 开始,如果转到 Int16 的定义,您将看到以下内容。
② Int32
因为它是 32 位的,所以它将存储 232个数字,它将同时存储正值和负值。正数将从 0 到 2^31 - 1,负数将从 -1 到 2^31,如果转到 Int32 的定义,看到以下内容。
③ Int64
因为它是 32 位的,所以它将存储 2^32个数字,它将同时存储正值和负值。正数将从 0 到 2^63 - 1,负数将从 -1 到 2^63,如果转到 Int32 的定义,看到以下内容。
我们分别定义以上3种数据类型:
当然,我们平常并没有这样定义数据,这三种数据类型也可以具有其他名称。例如,Int16 可以用作短数据类型short。Int32 可以用作 int 数据类型,而 Int64 可以用作长数据类型long,它们是等价的。
在 C# 中何时使用 Signed 以及何时使用无符号数据类型?
看情况,如果你只想存储正数,那么建议使用无符号数据类型,为什么,因为对于有符号短数据类型,你可以存储的最大正数是 32767,而对于无符号的 ushort 数据类型,你可以存储的最大正数是 65535。因此,使用相同的 2 个 Byes 内存,与 short 数据类型正数相比,我们有机会存储更大的正数,并且在 int 和 unit、long 和 ulong 的情况下也是如此。如果要同时存储正数和负数,则需要使用有符号数据类型。
3.3.2.6.浮点数
C#为我们提供了三种风格浮点数,分别是:Single (单精度)、Double (双精度)、Decimal (十进制浮点数,C#自己的数据类型)。
Single 数据类型占用 4 个字节,Double 数据类型占用 8 个字节,Decimal 占用 16 个字节的内存。为了更好地理解,请看下面的例子。为了创建一个单一的值,我们需要在数字的末尾添加后缀 f,同样,如果要创建一个十进制浮点数,则需要在值后缀 m(m大写或小写都可以)。如果您没有添加任何后缀,则默认情况下该值将为 double。
准确性:浮点数不如双精度和十进制准确,double 比 Float 更准确,但不如 Decimal 准确,Decimal 比 Float 和 Double 更准确。
3.3.3.引用数据类型
引用类型存储的是对象在内存中的地址,而不是实际的数据值。当你将一个引用类型的变量赋值给另一个变量时,实际上是在复制内存地址,而不是实际的数据。因此,两个变量将引用内存中的同一个对象。引用数据类型分为两种它们如下:
预定义类型 :包括 Objects、String 和 dynamics。
用户定义的类型 :包括类和接口。
3.3.3.1.类(Class)
类是最常用的引用类型,用于封装数据和行为。例如:
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
// 使用
Person person = new Person { Name = "John Doe", Age = 30 };
3.3.3.2.接口(Interface)
接口定义了一组方法的规范。例如:
public interface IAnimal
{
void Speak();
}
// 实现接口的类
public class Dog : IAnimal
{
public void Speak()
{
Console.WriteLine("Woof!");
}
}
3.3.3.3.数组(Array)
数组用于存储多个相同类型的元素。例如:
int[] numbers = new int[5]; // 创建一个整型数组
numbers[0] = 1; // 给数组的第一个元素赋值
3.3.3.4.委托(Delegate)
委托用于定义方法的签名和允许将方法作为参数传递。例如:
public delegate void MyDelegate(string message);
public void DisplayMessage(string message)
{
Console.WriteLine(message);
}
// 使用
MyDelegate myDelegate = DisplayMessage;
myDelegate("Hello, World!");
3.3.3.5.字符串(String)
在前面的示例中,我们讨论了在其中存储单个字符的 char 数据类型。现在,如果我尝试向 char 数据类型添加多个字符,那么我会得到编译时错误,如下图所示。
这时候我们引入了一个新的数据类型–字符串。字符串只不过是一系列 char 数据类型。现在,您可能有一个问题,如何知道字符串的大小?这很简单,首先,您需要知道字符串的长度,即有多少个字符,然后您需要将长度乘以 char 数据类型的大小,因为 String 只不过是一系列 char 数据类型。为了更好地理解,请看下面的例子:
我们转到string的定义出,可以看出string是一个引用类型。字符串是不可变的引用类型。例如:
string greeting = "Hello";
string name = "World";
string message = greeting + ", " + name + "!"; // 拼接字符串
Console.WriteLine(message); // 输出: Hello, World!
3.3.3.6.动态类型(Dynamic)
动态类型允许在运行时确定对象的类型。例如:
dynamic dyn = GetSomeObject(); // 假设这个函数返回一个不确定类型的对象
dyn.SomeMethod(); // 在运行时解析这个方法调用
3.3.4.指针类型
虽然指针(Pointer)在C#中也是一种引用类型,但由于其复杂性和潜在的安全风险,通常不建议在常规编程中使用指针,除非在处理与底层系统资源交互或特定性能优化的场景中。
如果我们像c++ 一样在c #中使用指针就会报错。
所以,在C#中使用指针通常需要在项目设置中启用“允许不安全代码”。要编译和运行此代码,你需要在项目属性中启用不安全代码。在Visual Studio中,你可以通过以下步骤来启用:右键点击项目 -> 属性;在“生成”选项卡下,找到“允许不安全代码”并勾选;保存项目属性并重新编译项目。
运行下面的代码
namespace PointerExample
{
class Program
{
static unsafe void Main(string[] args)
{
int value = 42;
int* pValue = &value; // 获取value的地址,并存储在指针变量pValue中
Console.WriteLine("Value: " + value); // 输出原始值
Console.WriteLine("Address of value: " + (IntPtr)pValue); // 输出变量的内存地址
Console.WriteLine("Value via pointer: " + *pValue); // 通过指针访问值
// 通过指针修改内存中的值
*pValue = 100;
Console.WriteLine("New value: " + value); // 输出修改后的值
// 使用完毕后,建议将指针设为null,避免野指针的出现
pValue = null;
}
}
}
3.3.5.实例(所有数据类型)
byte a = 56;//定义一个byte数据
Console.WriteLine(a);
Console.WriteLine(sizeof(byte));//查看byte数据的大小
sbyte b = -128;//定义一个byte数据
Console.WriteLine(b);
Console.WriteLine(sizeof(sbyte));//查看byte数据的大小
char c1 = 'a';
Console.WriteLine(c1);
Console.WriteLine(sizeof(char));
byte c2 = 85;
Console.WriteLine(c2);
Console.WriteLine((char)c2);
string str = "ABC";
var count = str.Length * sizeof(Char);
Console.WriteLine(str);
Console.WriteLine(str.Length * sizeof(char));
Int16 i1 = 12;
Int32 i2 = 23;
Int64 i3 = 6419;
Console.WriteLine(i1);
Console.WriteLine(i2);
Console.WriteLine(i3);
short i4 = 12;
int i5 = 23;
long i6 = 6419;
Console.WriteLine(i4);
Console.WriteLine(i5);
Console.WriteLine(i6);
float f1 = 1.0f;
double f2 = 23.999;
decimal f3 = 136.112222222222222m;
Console.WriteLine(f1);
Console.WriteLine(f2);
Console.WriteLine(f3);
namespace PointerExample
{
class Program
{
static unsafe void Main(string[] args)
{
int value = 42;
int* pValue = &value; // 获取value的地址,并存储在指针变量pValue中
Console.WriteLine("Value: " + value); // 输出原始值
Console.WriteLine("Address of value: " + (IntPtr)pValue); // 输出变量的内存地址
Console.WriteLine("Value via pointer: " + *pValue); // 通过指针访问值
// 通过指针修改内存中的值
*pValue = 100;
Console.WriteLine("New value: " + value); // 输出修改后的值
// 使用完毕后,建议将指针设为null,避免野指针的出现
pValue = null;
}
}
}
3.4.特殊的字符串
字符串在C#中有一些特殊的行为,因为它既是引用类型,又具有不可变性。下面详细解释字符串的这些特殊性以及与值类型、引用类型、值传递、引用传递、可变类型和不可变类型的关系。
3.4.1.字符串的引用类型特性
引用类型:字符串在C#中是一个引用类型。这意味着string类型的变量存储的是指向实际字符串数据的内存地址,而不是字符串本身的数据。例如,当你声明一个字符串变量时,实际的字符串数据存储在堆内存中,而变量只保存该数据的内存地址。
string str1 = "hello";
string str2 = str1; // str2 和 str1 引用同一个字符串对象
引用传递:当你将一个字符串作为参数传递给方法时,传递的是这个字符串的引用(即内存地址)。不过,由于字符串的不可变性,方法内部的任何修改都会创建一个新的字符串对象,而不是修改原始的字符串对象。
void ModifyString(string s)
{
s = "world"; // 创建了一个新的字符串对象
}
string str = "hello";
ModifyString(str);
Console.WriteLine(str); // 输出 "hello",原始字符串未被修改
3.4.2.字符串的不可变性
不可变性:字符串在C#中是不可变的。一旦创建,字符串的内容就不能被更改。任何看似对字符串的修改操作,实际上都会生成一个新的字符串对象,而原有字符串对象保持不变。
string str1 = "hello";
string str2 = str1.ToUpper(); // str2 是一个新对象,包含 "HELLO"
Console.WriteLine(str1); // 输出 "hello"
驻留池(Intern Pool):为了优化内存使用,C#会将字符串字面量存储在一个称为字符串驻留池(intern pool)的共享内存区域中。如果在程序中多次使用相同的字符串字面量,CLR会确保所有这些引用指向同一个内存地址。例如:
string str1 = "hello";
string str2 = "hello";
bool isSameReference = Object.ReferenceEquals(str1, str2); // 输出 true
在这个例子中,str1和str2引用的是驻留池中的同一个字符串对象,因此它们具有相同的引用。
3.4.3.字符串的值传递与引用传递
值传递的行为:尽管字符串是引用类型,但当你将字符串作为参数传递给方法时,由于其不可变性,它的行为更类似于值传递。因为任何修改都会生成一个新的字符串对象,并且不会影响原来的字符串引用。
void ModifyString(string s)
{
s = s + " world"; // 创建新字符串对象
}
string str = "hello";
ModifyString(str);
Console.WriteLine(str); // 输出 "hello"
在这个例子中,尽管str作为引用类型被传递给ModifyString方法,但方法内部对字符串的修改并不会改变原始的str变量,而是生成一个新的字符串。
3.4.4. 字符串的可变性问题
虽然字符串本身是不可变的,但有时候你可能需要频繁修改字符串内容。在这种情况下,使用不可变的字符串可能会导致大量的临时对象创建和内存分配,这会影响性能。为了解决这个问题,C#提供了StringBuilder类。
StringBuilder:StringBuilder是一个可变的字符串类型,它允许对字符串内容进行高效的修改,而不会像string那样频繁创建新的对象。
using System.Text;
StringBuilder sb = new StringBuilder("hello");
sb.Append(" world");
Console.WriteLine(sb.ToString()); // 输出 "hello world"
使用StringBuilder时,你可以反复修改同一个对象,而不是创建新的对象,从而提高性能。
3.4.5.总结:字符串的特殊性
(1)引用类型:字符串是引用类型,存储的是内存地址(引用)。
(2)不可变性:字符串是不可变的,任何修改都会生成一个新的字符串对象。
(3)驻留池优化:相同的字符串字面量会被共享存储在驻留池中,节省内存。
(4)值传递的行为:尽管是引用类型,由于不可变性,字符串的传递行为类似于值传递。
(5)性能优化:在需要频繁修改字符串时,可以使用StringBuilder来避免不必要的内存开销。
3.5.C#中的字面量
字面量(Literal)是代码中直接写出来的固定值。简单来说,就是你在代码里写的实际数据,比如数字、字符、字符串等。C# 中常见的字面量有整数、浮点数、字符、字符串和布尔值。
3.5.1.字面量和数据类型的关系
字面量就像是你放在容器里的具体物品。例如:一个苹果、一瓶水、一本书。在代码中,这些物品就是具体的值:42
是一个具体的数字,“Hello” 是一个具体的字符串,‘A’ 是一个具体的字符。
数据类型就像是描述这些容器的标签,它们告诉我们这些容器里应该放什么样的物品。例如:一个装水果的篮子、一个装液体的瓶子、一个装书的盒子。在代码中,数据类型描述了变量或常量可以保存什么样的数据:int
表示一个整数类型,可以存放像 42
这样的数字,string
表示一个字符串类型,可以存放像 "Hello"
这样的文本,char
表示一个字符类型,可以存放像 ‘A’ 这样的单个字符
3.5.2.整数字面量
整数字面量用于表示整数值。它可以是十进制、十六进制或二进制格式。
十进制字面量:不带前缀的数字,例如 42、0、-7。
十六进制字面量:以 0x 或 0X 开头的数字,例如 0x2A(等于十进制的 42)。
二进制字面量:以 0b 或 0B 开头的数字,例如 0b101010(也等于十进制的 42)。
int decimalLiteral = 42;
int hexLiteral = 0x2A;
int binaryLiteral = 0b101010;
3.5.3.浮点数字面量
浮点数字面量用于表示具有小数部分的数值。可以使用 f 或 F 表示单精度(float),使用 d 或 D 表示双精度(double),默认情况下,浮点数字面量被认为是 double 类型。还可以使用 m 或 M 表示十进制类型(decimal)。
double doubleLiteral = 3.14;
float floatLiteral = 3.14f;
decimal decimalLiteral = 3.14m;
3.5.4.字符字面量
字符字面量用于表示单个字符,用单引号 ’ 括起来。例如:
char charLiteral = 'A';
char escapeLiteral = '\n'; // 换行符
常见的转义字符包括:
':单引号
":双引号
\:反斜杠
\n:换行
\t:制表符
3.5.5.字符串字面量
字符串字面量用于表示一串字符,用双引号 " 括起来。可以使用转义字符或逐字字符串(以 @ 开头)表示特殊字符。
string stringLiteral = "Hello, World!";
string escapedString = "He said, \"Hello, World!\"";
string verbatimString = @"C:\Users\Name\Documents\File.txt";
3.5.6.布尔字面量
布尔字面量用于表示 true 或 false 值。
bool trueLiteral = true;
bool falseLiteral = false;
3.5.7.空字面量
空字面量用于表示空值,特别是在引用类型中,使用关键字 null。
string nullLiteral = null;
3.5.8.字面量类型的综合案例
以下是一个综合示例,展示了如何在实际代码中使用各种类型的字面量:
int decimalNumber = 123;
int hexNumber = 0x7B;
float pi = 3.14f;
char letter = 'A';
string message = "Hello, World!";
bool isTrue = true;
string path = @"C:\Program Files\MyApp";
Console.WriteLine($"Decimal: {decimalNumber}");
Console.WriteLine($"Hex: {hexNumber}");
Console.WriteLine($"Float: {pi}");
Console.WriteLine($"Char: {letter}");
Console.WriteLine($"String: {message}");
Console.WriteLine($"Boolean: {isTrue}");
Console.WriteLine($"Path: {path}");
运行上述代码将输出:
Decimal: 123
Hex: 123
Float: 3.14
Char: A
String: Hello, World!
Boolean: True
Path: C:\Program Files\MyApp
3.6.C#中的类型转换
3.6.1.计算机存储的基本单位:字节
我们知道一个字节(Byte)有8个比特(bit)构成,比特是存储的最小单位,表示0和1,但为什么计算机存储的基本单位是字节,而不是比特呢?
假设我们要存储数字3(二进制:11),就得组织两个比特的内存才能表示,如果是255(二进制:1111 1111),就要组织8个比特的数据。数字如果更大的话,需要组织的比特位就越多,那样太麻烦了,效率也太低。那么为什么不用Kbit、Mb、Gb等作为基本单位呢?不是不可以,存储一个数字1,你用Mb作为基本单位,好多内存空间是用不到的,浪费啊。
所以要选择一个存储单位,既方便存储和管理、又能充分利用内存空间的单位。用8个比特构成一个字节,并用字节作为计算机存储的基本单位,这样是很合适的,不会因为携带的信息粒度太小而造成管理内存困难或效率低下,也不因为信息粒度太大,造成存储空间浪费。
下面这张图展示了数据在一个字节中采用二进制的存储方式,高位在前,低位在后。将每一个比特位中的值进行相加:128 + 4 + 2 = 134,所以1000 0110 表示的十进制数据是134。
3.6.2.存储int数据类型
int数据类型占4个字节,每个字节8位,也是高位在前低位在后,比如
3.6.3.存储float数据类型
假设我们要存储浮点数 -12.5,其在内存中的存储方式如下:
(1)十进制转二进制:12.5 的二进制表示为 1100.1,具体方法如下:
//a.将整数部分 12 转换为二进制:
12 ÷ 2 = 6 余 0
6 ÷ 2 = 3 余 0
3 ÷ 2 = 1 余 1
1 ÷ 2 = 0 余 1
逆序排列余数得到二进制整数部分:1100
//b.将小数部分 0.5 转换为二进制:小数部分乘以2,得到的结果再取小数部分乘以2
0.5 × 2 = 1.0 → 取整数部分 1
//c.将 12.5 的二进制表示组合起来:1100.1
(2)将其转换为标准化的科学计数法表示:1.1001 × 2^3。
(3)确定符号位:符号位 S 为 1(因为是负数)。
(4)确定指数位:指数 3 加上偏移量 127 得到 130。二进制表示 130 为 10000010。
(5)确定尾数位:尾数位为 10010000000000000000000(去掉隐含的第一个 1 后补足到 23 位)。
(6)组合:
名称 比特位数目 二进制值
- 符号位 1 位 1
- 指数位 8 位 10000010
- 尾数位 23 位 10010000000000000000000
最终,-12.5 在内存中的二进制表示为:
1 10000010 10010000000000000000000
3.6.4.存储其它数据类型
C#中还有其他数据类型,比如float,double,char,string等,都是采用字节组合的形式存储,高位在前、低位在后。float占4个字节,double占8个字节,char占1个字节,string是多个char类型的字符构成,有几个字符,长度就为几。
3.6.5.C#中的类型转换
上面介绍了不同的数据在计算机中的存储方式,有了这些知识储备我们就好理解类型转换了。所谓类型转换就是从一种数据类型转到另外一种数据类型
在 C# 中,类型转换分为隐式转换和显式转换。
3.6.5.1.隐式转换(Implicit Conversion)
隐式转换是编译器自动进行的转换,这种转换不会导致数据丢失,主要用于从较小类型到较大类型的转换。简答理解就是将小盒子里的东西装到大盒子中,例如:
- byte → short(1个字节数据放到2个字节数据中)
- short → int(2个字节数据放到4个字节数据中)
- int → long(4个字节数据放到8个字节数据中)
- float → double(4个字节数据放到8个字节数据中)
int numInt = 123;
double numDouble = numInt; // 隐式转换
Console.WriteLine($"numDouble: {numDouble}"); // 输出: numDouble: 123
在上述示例中,int 类型的 numInt 自动转换为 double 类型的 numDouble。这种转换不会丢失数据,因为 double 类型比 int 类型范围更大。
3.6.5.2.显式转换(Explicit Conversion)
显式转换需要使用强制转换操作符 (dataType),可能会导致数据丢失,主要用于从较大类型到较小类型的转换。例如:
- double → float
- long → int
- int → short
double numDouble = 123.45;
int numInt = (int)numDouble; // 显式转换
Console.WriteLine($"numInt: {numInt}"); // 输出: numInt: 123
在上述示例中,double 类型的 numDouble 强制转换为 int 类型的 numInt,小数部分被舍弃,可能导致数据丢失。
3.6.5.3.类型转换的具体示例
数值类型的隐式转换
byte a = 10;
short b = a; // byte 隐式转换为 short
int c = b; // short 隐式转换为 int
long d = c; // int 隐式转换为 long
float e = d; // long 隐式转换为 float
double f = e; // float 隐式转换为 double
Console.WriteLine($"double: {f}"); // 输出: double: 10
数值类型的显式转换
double x = 123.45;
float y = (float)x; // double 显式转换为 float
long z = (long)y; // float 显式转换为 long
int w = (int)z; // long 显式转换为 int
short v = (short)w; // int 显式转换为 short
byte u = (byte)v; // short 显式转换为 byte
Console.WriteLine($"byte: {u}"); // 输出: byte: 123
3.6.6.使用Convert 类进行类型转换
C# 提供了 Convert 类,用于执行更安全和更全面的类型转换,适用于各种数据类型之间的转换,下面是一些常见的使用场景及示例,具体有哪些方法可以去Convert 类的定义出查看。
3.6.6.1.数值类型转换
// int 转换为 double
int intValue = 123;
double doubleValue = Convert.ToDouble(intValue);
Console.WriteLine($"int to double: {doubleValue}");
// double 转换为 int
double doubleValue2 = 123.45;
int intValue2 = Convert.ToInt32(doubleValue2);
Console.WriteLine($"double to int: {intValue2}");
// string 转换为 int
string strValue = "456";
int intValue3 = Convert.ToInt32(strValue);
Console.WriteLine($"string to int: {intValue3}");
// string 转换为 double
string strValue2 = "789.01";
double doubleValue3 = Convert.ToDouble(strValue2);
Console.WriteLine($"string to double: {doubleValue3}");
3.6.6.2.布尔类型转换
// string 转换为 bool
string trueString = "true";
bool boolValue = Convert.ToBoolean(trueString);
Console.WriteLine($"string to bool: {boolValue}");
// int 转换为 bool
int zero = 0;
int one = 1;
bool boolValue2 = Convert.ToBoolean(zero);
bool boolValue3 = Convert.ToBoolean(one);
Console.WriteLine($"int 0 to bool: {boolValue2}");
Console.WriteLine($"int 1 to bool: {boolValue3}");
3.6.6.3.日期类型转换
// string 转换为 DateTime
string dateString = "2024-07-06";
DateTime dateValue = Convert.ToDateTime(dateString);
Console.WriteLine($"string to DateTime: {dateValue}");
// DateTime 转换为 string
DateTime now = DateTime.Now;
string dateString2 = Convert.ToString(now);
Console.WriteLine($"DateTime to string: {dateString2}");
3.6.6.4.其他类型转换
// char 转换为 int
char charValue = 'A';
int intValue = Convert.ToInt32(charValue);
Console.WriteLine($"char to int: {intValue}");
// int 转换为 char
int intValue2 = 66;
char charValue2 = Convert.ToChar(intValue2);
Console.WriteLine($"int to char: {charValue2}");
// object 转换为 string
object objValue = 123;
string strValue = Convert.ToString(objValue);
Console.WriteLine($"object to string: {strValue}");
3.6.7.Parse 和 TryParse 方法
在 C# 中,Parse 和 TryParse 方法用于将字符串转换为数值类型,如整数、浮点数等。这两个方法的主要区别在于错误处理机制。
3.6.7.1.Parse 方法
Parse 方法用于将字符串转换为指定的数据类型,如果转换失败,则会抛出异常。
适用场景: 适用于可以确定输入字符串格式正确的情况下使用。
string str = "123";
int number = int.Parse(str); // 成功转换
Console.WriteLine(number); // 输出: 123
string invalidStr = "abc";
int invalidNumber = int.Parse(invalidStr); // 抛出 FormatException
3.6.7.2.TryParse 方法
TryParse 方法尝试将字符串转换为指定的数据类型,并返回一个布尔值指示转换是否成功。它不会抛出异常,而是通过输出参数返回转换结果。
适用场景: 适用于不确定输入字符串格式是否正确的情况下使用,避免程序因异常而崩溃。
string str = "123";
bool success = int.TryParse(str, out int number);
if (success)
{
Console.WriteLine(number); // 输出: 123
}
else
{
Console.WriteLine("转换失败");
}
string invalidStr = "abc";
success = int.TryParse(invalidStr, out int invalidNumber);
if (success)
{
Console.WriteLine(invalidNumber);
}
else
{
Console.WriteLine("转换失败"); // 输出: 转换失败
}
3.6.7.3.方法比较
*特点* | *Parse* | *TryParse* |
---|---|---|
返回值 | 转换后的值 | 布尔值(成功或失败) |
错误处理 | 抛出异常 | 不抛异常,返回 false |
性能 | 较慢(抛异常) | 较快(无异常) |
适用场景 | 确信格式正确 | 格式不确定或健壮性需求 |
3.7.C# 中的操作符
3.7.1.什么是操作符?
操作符是用于执行操作的符号。C#提供了丰富的操作符,用于各种常见的编程任务。操作符可以应用于变量和值,执行算术、比较、逻辑、位、赋值和其他操作。
3.7.2.算术操作符
算术操作符用于执行基本的数学运算。C#中的常见算术操作符如下:
*操作符* | *描述* | *示例* |
---|---|---|
+ | 加法 | a + b |
- | 减法 | a - b |
* | 乘法 | a * b |
/ | 除法 | a / b |
示例代码:
int a = 10;
int b = 3;
int sum = a + b; // 13
int difference = a - b; // 7
int product = a * b; // 30
int quotient = a / b; // 3
int remainder = a % b; // 1
a++; // 11
b--; // 2
3.7.3.关系操作符
关系操作符用于比较两个值或变量。C#中的常见关系操作符如下:
*操作符* | *描述* | *示例* |
---|---|---|
== | 等于 | a == b |
!= | 不等于 | a != b |
> | 大于 | a > b |
< | 小于 | a < b |
示例代码:
int a = 10;
int b = 3;
bool isEqual = (a == b); // false
bool isNotEqual = (a != b); // true
bool isGreater = (a > b); // true
bool isLess = (a < b); // false
bool isGreaterOrEqual = (a >= b); // true
bool isLessOrEqual = (a <= b); // false
3.7.4.逻辑操作符
逻辑操作符用于执行布尔逻辑运算。C#中的常见逻辑操作符如下:
*操作符* | *描述* | *示例* |
---|---|---|
&& | 逻辑与(AND) | a && b |
|| | 逻辑或(OR) | a || b |
! | 逻辑非(NOT) | !a |
示例代码:
bool a = true;
bool b = false;
bool result1 = a && b; // false
bool result2 = a || b; // true
bool result3 = !a; // false
3.7.5.位操作符
位操作符用于按位(比特位)操作整数类型的数据,所以位操作符都是操作二进制数的。C#中的常见位操作符如下:
*操作符* | *描述* | *示例* |
---|---|---|
& | 按位与(AND) | a & b |
| | 按位或(OR) | a | b |
^ | 按位异或(XOR) | a ^ b |
~ | 按位取反(NOT) | ~a |
<< | 左移 | a << 2 |
>> | 右移 | a >> 2 |
示例代码:
int a = 5; // 0101
int b = 3; // 0011
int result1 = a & b; // 0001 (1)
int result2 = a | b; // 0111 (7)
int result3 = a ^ b; // 0110 (6)
int result4 = ~a; // 1010 (按位取反)
int result5 = a << 2; // 10100 (20)
int result6 = a >> 2; // 0001 (1)
位操作的左移、右移如下图所示:
3.7.6.赋值操作符
赋值操作符用于给变量赋值。C#中的常见赋值操作符如下:
*操作符* | *描述* | *示例* | *等价于* |
---|---|---|---|
= | 简单赋值 | a = b | |
+= | 加且赋值 | a += b | a = a + b |
-= | 减且赋值 | a -= b | a = a - b |
*= | 乘且赋值 | a *= b | a = a * b |
/= | 除且赋值 | a /= b | a = a / b |
%= | 取模且赋值 | a %= b | a = a % b |
&= | 按位与且赋值 | a &= b | a = a & b |
|= | 按位或且赋值 | a |= b | a = a | b |
^= | 按位异或且赋值 | a ^= b | a = a ^ b |
<<= | 左移且赋值 | a <<= 2 | a = a << 2 |
>>= | 右移且赋值 | a >>= 2 | a = a >> 2 |
示例代码:
int a = 10;
a += 5; // 15
a -= 3; // 12
a *= 2; // 24
a /= 4; // 6
a %= 3; // 0
a &= 2; // 0
a |= 1; // 1
a ^= 3; // 2
a <<= 2; // 8
a >>= 1; // 4
3.7.7.条件操作符
条件操作符也称三元操作符,用于根据条件表达式的真假值选择不同的结果。语法如下:
condition ? trueResult : falseResult;
示例代码:
int a = 10;
int b = 5;
int max = (a > b) ? a : b; // max = 10
3.7.8.其他操作符
C#中还有一些其他的操作符,如typeof、sizeof、is、as等。
3.7.8.1.typeof
typeof用于获取类型的Type对象。
Type t = typeof(int); // t = System.Int32
3.7.8.2.sizeof
sizeof用于获取值类型的大小。
int size = sizeof(int); // size = 4
3.7.8.3.is
is用于检查对象是否是某个类型。
object obj = "hello";
bool isString = obj is string; // true
3.7.8.4.as
as用于进行类型转换,如果转换失败返回null。
object obj = "hello";
string str = obj as string; // str = "hello"
3.7.9.运算符的优先级和结合性
运算符的优先级和结合性决定了运算的执行顺序。在表达式中,具有较高优先级的运算符先于优先级较低的运算符执行。结合性决定了具有相同优先级的运算符的计算顺序。运算符优先级表如下图所示,但我们记不住优先级时,加个小括号就行了。以下是C#中常见运算符的优先级表,从高到低排列:
表 2-1 运算符优先级表
*优先级* | *运算符* | *描述* | *结合性* |
---|---|---|---|
1 | ( ) | 括号 | 从左到右 |
2 | ++, --, !, ~ | 单目运算符 | 从右到左 |
3 | *, /, % | 乘法、除法、取模 | 从左到右 |
4 | +, - | 加法、减法 | 从左到右 |
5 | <<, >> | 移位运算 | 从左到右 |
6 | <, <=, >, >= | 关系运算符 | 从左到右 |
7 | ==, != | 相等运算符 | 从左到右 |
8 | & | 按位与 | 从左到右 |
9 | ^ | 按位异或 | 从左到右 |
10 | | | 按位或 | 从左到右 |
11 | && | 逻辑与 | 从左到右 |
示例代码:
int a = 10;
int b = 5;
int c = a + b * 2; // c = 20,乘法优先于加法
int d = (a + b) * 2; // d = 30,括号改变了优先级
3.7.10. 运算符重载
3.7.10.1.运算符重载语法
运算符重载允许为自定义类型(类或结构体)定义特定的运算行为。运算符重载方法必须是 public 和 static 的,并且包含 operator 关键字。下面是运算符重载的基本语法:
public static 返回类型 operator 运算符(参数列表)
{
// 方法体
}
以下是一个重载+运算符的示例,我们定义了一个Complex类,代表复数。然后重载了+运算符,使其可以用于两个Complex对象的相加操作。
public class Complex
{
public double Real { get; set; }
public double Imaginary { get; set; }
public Complex(double real, double imaginary)
{
Real = real;
Imaginary = imaginary;
}
// 重载 + 运算符
public static Complex operator +(Complex c1, Complex c2)
{
return new Complex(c1.Real + c2.Real, c1.Imaginary + c2.Imaginary);
}
}
public class Program
{
public static void Main()
{
Complex c1 = new Complex(1.0, 2.0);
Complex c2 = new Complex(3.0, 4.0);
Complex result = c1 + c2;
Console.WriteLine($"Result: {result.Real} + {result.Imaginary}i");
}
}
3.7.10.2.可重载的运算符
C#中几乎所有的运算符都可以重载,但以下几个运算符除外:
(1)条件运算符(&&, ||, ?:)
(2)赋值运算符(=)
3.7.10.3.运算符重载的限制
(1)重载运算符必须是public和static的。
(2)重载运算符必须至少有一个参数是类或结构体类型。
(3)重载运算符不能更改运算符的优先级和结合性。
3.8.控制流语句
控制流语句用于控制程序的执行顺序。C# 提供了三种控制流语句:包括条件语句、循环语句和跳转语句。我们用个例子来解释什么是控制流,想象一下你正在厨房里准备一顿饭。你有一份食谱,告诉你如何一步一步地准备和烹饪这顿饭。
条件语句:根据材料或情况的不同,你可能会选择不同的步骤。例如,如果你有鸡肉,你就做鸡肉菜,如果你有牛肉,你就做牛肉菜。这就像 if 语句根据条件决定执行哪段代码。
循环语句:在食谱中被要求重复某个步骤,直到某个条件满足为止。例如,你可能需要搅拌汤,直到它变稠。这就像 while 语句,一直重复执行代码块,直到条件为假。
跳转语句:你可以跳过某些步骤或者提前结束某个步骤。例如,如果你尝了一口汤,觉得味道正好,你可能会直接跳到最后一步,而不需要再加盐。这就像 break 语句用于提前退出循环。
3.8.1.条件语句
条件语句根据表达式的真假值决定执行哪个代码块。C# 中常见的条件语句包括 if、else if、else 和 switch。
3.8.1.1.if 语句
if 语句用于在条件表达式为真时执行某个代码块。
int a = 10;
if (a > 5)
{
Console.WriteLine("a 大于 5");
}
3.8.1.2.else if 语句
else if 语句用于在第一个条件表达式为假但另一个条件表达式为真时执行代码块。
int a = 10;
if (a > 10)
{
Console.WriteLine("a 大于 10");
}
else if (a == 10)
{
Console.WriteLine("a 等于 10");
}
3.8.1.3.else 语句
else 语句用于在所有条件表达式都为假时执行代码块。
int a = 10;
if (a > 10)
{
Console.WriteLine("a 大于 10");
}
else if (a == 10)
{
Console.WriteLine("a 等于 10");
}
else
{
Console.WriteLine("a 小于 10");
}
3.8.1.4.switch 语句
switch 语句根据表达式的值选择一个代码块执行。
int a = 2;
switch (a)
{
case 1:
Console.WriteLine("a 是 1");
break;
case 2:
Console.WriteLine("a 是 2");
break;
case 3:
Console.WriteLine("a 是 3");
break;
default:
Console.WriteLine("a 不是 1, 2, 或 3");
break;
}
3.8.1.5.嵌套条件语句
条件语句可以嵌套在一起使用,例如:
int a = 10;
if (a > 5)
{
if (a < 15)
{
Console.WriteLine("a 在 5 和 15 之间");
}
else
{
Console.WriteLine("a 大于或等于 15");
}
}
else
{
Console.WriteLine("a 小于或等于 5");
}
3.8.2. 循环语句
循环语句用于重复执行一个代码块,直到指定条件为假。C# 中常见的循环语句包括 for、while、do-while 和 foreach。
3.8.2.1.for 语句
for 语句用于在循环开始前初始化循环变量,在每次迭代前检查条件表达式,并在每次迭代后更新循环变量。
for (int i = 0; i < 5; i++)
{
Console.WriteLine($"i = {i}");
}
3.8.2.2.while 语句
while 语句在每次迭代前检查条件表达式,如果条件为真则执行代码块。
int i = 0;
while (i < 5)
{
Console.WriteLine($"i = {i}");
i++;
}
3.8.2.3.do-while 语句
do-while 语句类似于 while 语句,但它在每次迭代后检查条件表达式。这意味着代码块至少会执行一次。
int i = 0;
do
{
Console.WriteLine($"i = {i}");
i++;
} while (i < 5);
3.8.2.4.foreach 语句
foreach 语句用于遍历集合中的每个元素。
int[] numbers = { 1, 2, 3, 4, 5 };
foreach (int number in numbers)
{
Console.WriteLine($"number = {number}");
}
3.8.3.跳转语句
跳转语句用于直接改变程序的执行流程。C# 中常见的跳转语句包括 break、continue、return 和 goto。
3.8.3.1.break 语句
break 语句用于立即退出循环或 switch 语句。
for (int i = 0; i < 5; i++)
{
if (i == 3)
{
break; // 退出循环
}
Console.WriteLine($"i = {i}");
}
3.8.3.2. continue 语句
continue 语句用于跳过当前迭代并开始下一次迭代。
for (int i = 0; i < 5; i++)
{
if (i == 3)
{
continue; // 跳过剩下的语句,开始下一次迭代
}
Console.WriteLine($"i = {i}");
}
3.8.3.3. return 语句
return 语句用于从方法中返回,并且可以返回一个值。
public int Add(int a, int b)
{
return a + b; // 返回 a 和 b 的和
}
int result = Add(2, 3);
Console.WriteLine($"result = {result}");
3.8.3.4.goto 语句
goto 语句用于无条件跳转到指定标签的位置。
int a = 10;
if (a == 10)
{
goto Label1; // 跳转到 Label1
}
Console.WriteLine("这段代码不会执行");
Label1:
Console.WriteLine("跳转到 Label1");
3.8.4.综合示例
结合以上所有控制流语句,编写一个综合示例:
using System;
public class Program
{
public static void Main()
{
// if-else 语句
int x = 10;
if (x > 5)
{
Console.WriteLine("x 大于 5");
}
else
{
Console.WriteLine("x 小于或等于 5");
}
// switch 语句
int y = 2;
switch (y)
{
case 1:
Console.WriteLine("y 是 1");
break;
case 2:
Console.WriteLine("y 是 2");
break;
default:
Console.WriteLine("y 不是 1 或 2");
break;
}
// for 循环
for (int i = 0; i < 3; i++)
{
Console.WriteLine($"for 循环中的 i = {i}");
}
// while 循环
int j = 0;
while (j < 3)
{
Console.WriteLine($"while 循环中的 j = {j}");
j++;
}
// do-while 循环
int k = 0;
do
{
Console.WriteLine($"do-while 循环中的 k = {k}");
k++;
} while (k < 3);
// foreach 循环
int[] numbers = { 1, 2, 3 };
foreach (int number in numbers)
{
Console.WriteLine($"foreach 循环中的 number = {number}");
}
// break 和 continue
for (int m = 0; m < 5; m++)
{
if (m == 2)
{
continue; // 跳过 m == 2
}
if (m == 4)
{
break; // 退出循环
}
Console.WriteLine($"break 和 continue 示例中的 m = {m}");
}
// return 语句
Console.WriteLine($"Add(2, 3) 的结果是 {Add(2, 3)}");
// goto 语句
int n = 10;
if (n == 10)
{
goto Label1; // 跳转到 Label1
}
Console.WriteLine("这段代码不会执行");
Label1:
Console.WriteLine("跳转到 Label1");
}
public static int Add(int a, int b)
{
return a + b;
}
}
3.9.方法
3.9.1.什么是方法?
方法是执行特定任务的代码块,它接收输入参数(可为空)、执行操作并返回结果。
3.9.2.方法的定义
方法的定义包括方法的名称、返回类型、参数列表和方法体。下面是一个简单的方法定义示例:
public int Add(int a, int b)
{
return a + b;
}
表 2-2 方法的组成部分
*组成部分* | *描述* | *示例* |
---|---|---|
访问修饰符 | 确定方法的可访问性(例如,public、private) | public |
返回类型 | 方法返回的值的类型(例如,int、void) | int |
方法名称 | 标识方法的名称 | Add |
参数列表 | 方法接受的输入参数(例如,int a, int b) | int a, int b |
方法体 | 包含方法执行的代码块 | { return a + b; } |
我们再创建一个无返回值的方法,方法如下:
public void PrintMessage(string message)
{
Console.WriteLine(message);
}
在上述示例中,PrintMessage 方法接受一个字符串参数,并将其输出到控制台。该方法没有返回值,因此返回类型为 void。
3.9.3.调用方法
调用方法是通过其名称和参数列表来执行方法的操作。下面是一个调用方法的示例:
public class Program
{
public static void Main()
{
Program program = new Program();
int result = program.Add(2, 3);
Console.WriteLine($"Result: {result}");
}
public int Add(int a, int b)
{
return a + b;
}
}
在上述示例中,Add 方法被 Main 方法调用,并传递了参数 2 和 3,然后输出结果 5。
3.9.4.方法的重载
方法的重载允许在同一类中定义多个同名方法,但具有不同的参数列表。重载方法必须具有不同数量或类型的参数。
public int Add(int a, int b)
{
return a + b;
}
public double Add(double a, double b)
{
return a + b;
}
在上述示例中,定义了两个 Add 方法,一个接受 int 类型参数,另一个接受 double 类型参数。
public class Program
{
public static void Main()
{
Program program = new Program();
int intResult = program.Add(2, 3);
double doubleResult = program.Add(2.5, 3.5);
Console.WriteLine($"Int Result: {intResult}");
Console.WriteLine($"Double Result: {doubleResult}");
}
public int Add(int a, int b)
{
return a + b;
}
public double Add(double a, double b)
{
return a + b;
}
}
3.9.5.参数的传递
方法可以通过值或引用传递参数。
3.9.5.1.值传递
值传递将参数的副本传递给方法,方法对参数的修改不会影响原始值。
public void Increment(int a)
{
a++;
}
public static void Main()
{
int number = 5;
Increment(number);
Console.WriteLine($"Number: {number}"); // 输出 5
}
3.9.5.2.引用传递
引用传递将参数的引用传递给方法,在方法中对参数的修改会影响原始值。使用 ref 或 out 关键字实现引用传递。ref和out的区别在于:ref要求在方法调用前变量已经初始化,而out则不需要在方法调用前初始化,但必须在方法内赋值。
(1)ref 引用传递
public void Increment(ref int a)
{
a++;
}
public static void Main()
{
int number = 5;
Increment(ref number);
Console.WriteLine($"Number: {number}"); // 输出 6
}
(2)out 引用传递
public void GetValues(out int a, out int b)
{
a = 10;
b = 20;
}
public static void Main()
{
int x, y;
GetValues(out x, out y);
Console.WriteLine($"x = {x}, y = {y}"); // 输出 x = 10, y = 20
}
3.9.6.返回值
方法可以返回一个值,返回类型由方法的返回类型指定。使用 void 关键字表示方法不返回值。
public int Add(int a, int b)
{
return a + b;
}
public void PrintMessage()
{
Console.WriteLine("Hello, World!");
}
方法不仅可以返回基本类型,还可以返回对象。
public class Person
{
public string Name { get; set; }
}
public Person CreatePerson(string name)
{
return new Person { Name = name };
}
public static void Main()
{
Program program = new Program();
Person person = program.CreatePerson("John Doe");
Console.WriteLine($"Person's name: {person.Name}");
}
3.9.7.可选参数
方法可以具有可选参数,如果调用方法时没有提供这些参数,默认值将被使用。
public void PrintMessage(string message = "Hello, World!")
{
Console.WriteLine(message);
}
public static void Main()
{
PrintMessage(); // 输出 "Hello, World!"
PrintMessage("Hi there!"); // 输出 "Hi there!"
}
3.9.8.参数数组
参数数组允许方法接受可变数量的参数。使用 params 关键字定义参数数组。
public void PrintNumbers(params int[] numbers)
{
foreach (int number in numbers)
{
Console.WriteLine(number);
}
}
public static void Main()
{
PrintNumbers(1, 2, 3, 4, 5); // 输出 1 2 3 4 5
}
3.9.9.方法的优缺点
3.9.9.1.优点
(1)模块化
方法将代码分成独立的块,使程序更易于理解和管理。例如,每个方法可以负责特定的任务,如数据处理、输入输出等。
(2)代码复用
方法可以在不同地方多次调用,减少代码重复。例如,一个 Add 方法可以在多个地方调用来执行加法操作。
(3)易于维护
代码集中在方法内部,修改某个功能只需更改相应的方法,降低出错概率。例如,如果需要修改加法逻辑,只需更改 Add 方法。
(4)提高可读性
方法名称和参数列表提供了代码的语义信息,使代码更易读。例如,CalculateArea(width, height) 清楚地表明其计算矩形面积。
(5)调试方便
独立的方法使得定位和修复错误更加简单。例如,可以单独测试 Add 方法来确保其正确性。
3.9.9.2.缺点
(1)方法调用开销:
每次调用方法都有一定的性能开销,如参数传递和堆栈管理。例如,在性能关键的代码中频繁调用小方法可能影响效率。
(2)过度使用方法
如果方法过多或过于细化,可能导致代码难以跟踪和理解。例如,将简单的逻辑拆分成太多方法会使程序复杂化。
(3)维护多个方法
随着项目的发展,维护和更新大量的方法可能变得困难。例如,需要确保所有相关方法在更改后仍然正常工作。
(4)依赖性增加
方法之间的依赖性可能导致修改一个方法时需要同步修改其他方法。例如,如果修改了 CalculateArea 方法的参数类型,所有调用该方法的地方也需要修改。
3.9.10.综合示例
结合以上所有内容,编写一个综合示例:
using System;
public class Program
{
public static void Main()
{
Program program = new Program();
// 调用 Add 方法
int intResult = program.Add(2, 3);
double doubleResult = program.Add(2.5, 3.5);
Console.WriteLine($"Int Result: {intResult}");
Console.WriteLine($"Double Result: {doubleResult}");
// 值传递示例
int number = 5;
program.Increment(number);
Console.WriteLine($"Number after Increment (Value): {number}"); // 输出 5
// 引用传递示例
program.Increment(ref number);
Console.WriteLine($"Number after Increment (Ref): {number}"); // 输出 6
// out 关键字示例
program.GetValues(out int x, out int y);
Console.WriteLine($"x = {x}, y = {y}"); // 输出 x = 10, y = 20
// 返回对象示例
Person person = program.CreatePerson("John Doe");
Console.WriteLine($"Person's name: {person.Name}");
// 可选参数示例
program.PrintMessage(); // 输出 "Hello, World!"
program.PrintMessage("Hi there!"); // 输出 "Hi there!"
// 参数数组示例
program.PrintNumbers(1, 2, 3, 4, 5); // 输出 1 2 3 4 5
}
public int Add(int a, int b)
{
return a + b;
}
public double Add(double a, double b)
{
return a + b;
}
public void Increment(int a)
{
a++;
}
public void Increment(ref int a)
{
a++;
}
public void GetValues(out int a, out int b)
{
a = 10;
b = 20;
}
public Person CreatePerson(string name)
{
return new Person { Name = name };
}
public void PrintMessage(string message = "Hello, World!")
{
Console.WriteLine(message);
}
public void PrintNumbers(params int[] numbers)
{
foreach (int number in numbers)
{
Console.WriteLine(number);
}
}
}
public class Person
{
public string Name { get; set; }
}
3.10.函数的递归调用
递归是一种让函数调用自己来解决问题的方法,特别适用于能被分解成更小部分的问题。本文将介绍递归的基本概念、使用方法、常见递归算法、优缺点及尾递归优化。
3.10.1.什么是递归?
递归是指在函数的定义中调用函数自身。递归函数通常有两个主要部分:
(1)基线条件(Base Case):定义函数何时停止递归调用的条件
。
(2)递归条件(Recursive Case):函数调用自身,以处理更小或更简单的子问题。
3.10.2.递归函数的结构
一个典型的递归函数结构如下:
returnType FunctionName(parameters)
{
if (baseCondition)
{
// 基线条件:停止递归
return baseResult;
}
else
{
// 递归条件:调用自身
return FunctionName(smallerProblem);
}
}
3.10.3.递归示例
3.10.3.1.计算阶乘
阶乘(Factorial)是递归的一种经典示例。阶乘定义如下:
0! = 1
n! = n * (n-1)! (对于 n > 0)
using System;
class Program
{
static void Main()
{
int number = 5;
int result = Factorial(number);
Console.WriteLine($"{number}! = {result}");
}
static int Factorial(int n)
{
if (n == 0)
{
return 1; // 基线条件
}
else
{
return n * Factorial(n - 1); // 递归调用
}
}
}
在这个示例中,当 n 等于 0 时,递归停止,返回 1。否则,函数调用自身,并将 n 减 1,直到 n 为 0。我们来看原理:
Main
|
Factorial(5)
|
5 * Factorial(4)
|
4 * Factorial(3)
|
3 * Factorial(2)
|
2 * Factorial(1)
|
1 * Factorial(0)
|
1 (Base Case)
回溯:
Factorial(1) 返回 1
Factorial(2) 返回 2 * 1 = 2
Factorial(3) 返回 3 * 2 = 6
Factorial(4) 返回 4 * 6 = 24
Factorial(5) 返回 5 * 24 = 120
3.10.3.2.计算斐波那契数列
斐波那契数列(Fibonacci Sequence)也是递归的一个常见示例。斐波那契数列定义如下:
F(0) = 0
F(1) = 1
F(n) = F(n-1) + F(n-2) (对于 n > 1)
using System;
class Program
{
static void Main()
{
int number = 10;
for (int i = 0; i <= number; i++)
{
Console.WriteLine($"F({i}) = {Fibonacci(i)}");
}
}
static int Fibonacci(int n)
{
if (n == 0)
{
return 0; // 基线条件
}
else if (n == 1)
{
return 1; // 基线条件
}
else
{
return Fibonacci(n - 1) + Fibonacci(n - 2); // 递归调用
}
}
}
在这个示例中,当 n 为 0 或 1 时,递归停止,分别返回 0 和 1。否则,函数调用自身两次,并将 n 减 1 和减 2。原理如下:
Fibonacci(4)
|
Fibonacci(3) + Fibonacci(2)
| |
F(2) + F(1) F(1) + F(0)
| | | |
F(1) + F(0) 1 0
| |
1 0
回溯:
F(2) = 1 + 0 = 1
F(3) = 1 + 1 = 2
F(2) = 1 + 0 = 1
F(4) = 2 + 1 = 3
3.10.4.递归的优点和缺点
3.10.4.1.优点
(1)简洁性:递归代码通常比迭代代码更简洁、更易读。
(2)自然性:某些问题(如树和图的遍历)使用递归更自然、更直观。
3.10.4.2.缺点
(1)性能:递归可能导致大量的函数调用,增加了时间和空间复杂度。
(2)堆栈溢出:递归调用太深可能导致堆栈溢出(Stack Overflow)。
(3)内存消耗:每次递归调用都会占用堆栈空间,对于深度递归可能会消耗大量内存。
3.10.5.尾递归优化
尾递归是递归的一种特殊情况,其中递归调用是函数中的最后一个操作。某些编译器可以优化尾递归,减少堆栈消耗。尾递归示例:
using System;
class Program
{
static void Main()
{
int number = 5;
int result = FactorialTailRecursion(number, 1);
Console.WriteLine($"{number}! = {result}");
}
static int FactorialTailRecursion(int n, int accumulator)
{
if (n == 0)
{
return accumulator; // 基线条件
}
else
{
return FactorialTailRecursion(n - 1, n * accumulator); // 尾递归调用
}
}
}
在这个示例中,FactorialTailRecursion 函数在每次调用时将结果累积传递给下一个递归调用,使得最后的递归调用是函数的最后一个操作。
3.10.6.常见的递归算法
3.10.6.1.二分查找
using System;
class Program
{
static void Main()
{
int[] sortedArray = { 1, 3, 5, 7, 9, 11, 13, 15, 17, 19 };
int target = 7;
int result = BinarySearch(sortedArray, target, 0, sortedArray.Length - 1);
Console.WriteLine($"Index of {target}: {result}");
}
static int BinarySearch(int[] array, int target, int low, int high)
{
if (low > high)
{
return -1; // 基线条件:未找到目标
}
int mid = (low + high) / 2;
if (array[mid] == target)
{
return mid; // 基线条件:找到目标
}
else if (array[mid] > target)
{
return BinarySearch(array, target, low, mid - 1); // 递归调用左半部分
}
else
{
return BinarySearch(array, target, mid + 1, high); // 递归调用右半部分
}
}
}
3.10.6.2.合并排序
using System;
class Program
{
static void Main()
{
int[] array = { 38, 27, 43, 3, 9, 82, 10 };
MergeSort(array, 0, array.Length - 1);
Console.WriteLine("Sorted array:");
foreach (var item in array)
{
Console.Write(item + " ");
}
}
static void MergeSort(int[] array, int left, int right)
{
if (left < right)
{
int middle = (left + right) / 2;
MergeSort(array, left, middle); // 递归调用左半部分
MergeSort(array, middle + 1, right); // 递归调用右半部分
Merge(array, left, middle, right); // 合并两部分
}
}
static void Merge(int[] array, int left, int middle, int right)
{
int leftArrayLength = middle - left + 1;
int rightArrayLength = right - middle;
int[] leftArray = new int[leftArrayLength];
int[] rightArray = new int[rightArrayLength];
Array.Copy(array, left, leftArray, 0, leftArrayLength);
Array.Copy(array, middle + 1, rightArray, 0, rightArrayLength);
int i = 0, j = 0, k = left;
while (i < leftArrayLength && j < rightArrayLength)
{
if (leftArray[i] <= rightArray[j])
{
array[k++] = leftArray[i++];
}
else
{
array[k++] = rightArray[j++];
}
}
while (i < leftArrayLength)
{
array[k++] = leftArray[i++];
}
while (j < rightArrayLength)
{
array[k++] = rightArray[j++];
}
}
}
3.10.6.3.组合生成
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
char[] set = { 'a', 'b', 'c' };
int k = 2;
List<string> result = new List<string>();
GenerateCombinations(set, "", 0, k, result);
Console.WriteLine("Combinations:");
foreach (var combination in result)
{
Console.WriteLine(combination);
}
}
static void GenerateCombinations(char[] set, string prefix, int start, int k, List<string> result)
{
if (k == 0)
{
result.Add(prefix);
return;
}
for (int i = start; i < set.Length; i++)
{
GenerateCombinations(set, prefix + set[i], i + 1, k - 1, result);
}
}
}
3.11.字符串
3.11.1.什么是字符串?
在 C# 中,字符串是一串字符的集合。字符串是不可变的,这意味着一旦创建,它的内容就不能被改变。每次对字符串进行操作时,都会生成一个新的字符串对象,而不是修改原来的字符串。字符串就像一串珠子,字符是串上的每一颗珠子。每颗珠子都有自己的位置(索引),你可以通过位置找到具体的珠子。一串珠子是不可变的,这意味着一旦你把珠子串起来,就不能直接改变某颗珠子。你如果要换一颗珠子,需要创建一串新的珠子。
string greeting = "Hello, World!";
Console.WriteLine(greeting);
3.11.2.字符串的声明和初始化
字符串可以通过直接赋值来声明和初始化。也可以使用构造函数来创建字符串对象。
// 直接赋值
string message = "Welcome to C# programming";
// 使用构造函数
char[] chars = { 'H', 'e', 'l', 'l', 'o' };
string greeting = new string(chars);
3.11.3.字符串的常见操作
C# 提供了丰富的方法来操作字符串,包括连接、比较、查找、替换、拆分等。
3.11.3.1.字符串连接
字符串连接可以使用 + 运算符或 String.Concat 方法。
string firstName = "John";
string lastName = "Doe";
string fullName = firstName + " " + lastName;
Console.WriteLine(fullName); // 输出 "John Doe"
// 使用 String.Concat
string fullNameConcat = String.Concat(firstName, " ", lastName);
Console.WriteLine(fullNameConcat); // 输出 "John Doe"
3.11.3.2.字符串比较
字符串比较可以使用 == 运算符、Equals 方法或 Compare 方法。
string str1 = "hello";
string str2 = "Hello";
// 使用 ==
bool isEqual = str1 == str2;
Console.WriteLine(isEqual); // 输出 False
// 使用 Equals 方法
bool isEqualsMethod = str1.Equals(str2, StringComparison.OrdinalIgnoreCase);
Console.WriteLine(isEqualsMethod); // 输出 True
// 使用 Compare 方法
int comparisonResult = String.Compare(str1, str2, StringComparison.OrdinalIgnoreCase);
Console.WriteLine(comparisonResult); // 输出 0
3.11.3.3.查找子字符串
查找子字符串可以使用 IndexOf 方法:
string message = "Welcome to C# programming";
int index = message.IndexOf("C#");
Console.WriteLine(index); // 输出 11
3.11.3.4.字符串替换
字符串替换可以使用 Replace 方法:
string message = "Hello, World!";
string newMessage = message.Replace("World", "C#");
Console.WriteLine(newMessage); // 输出 "Hello, C#!"
3.11.3.5.字符串拆分
字符串拆分可以使用 Split 方法:
string data = "apple,banana,orange";
string[] fruits = data.Split(',');
foreach (string fruit in fruits)
{
Console.WriteLine(fruit);
}
3.11.3.6.字符串截取
字符串截取可以使用 Substring 方法:
string message = "Welcome to C# programming";
string subMessage = message.Substring(11, 2);
Console.WriteLine(subMessage); // 输出 "C#"
3.11.4.字符串格式化
C# 提供了多种方法来格式化字符串,包括插值字符串、String.Format 方法和 String.Join 方法。
3.11.4.1.插值字符串
插值字符串使用 $ 符号,可以直接在字符串中嵌入表达式。
string name = "John";
int age = 30;
string message = $"Name: {name}, Age: {age}";
Console.WriteLine(message); // 输出 "Name: John, Age: 30"
3.11.4.2.String.Format 方法
String.Format 方法使用占位符来格式化字符串。
string name = "John";
int age = 30;
string message = String.Format("Name: {0}, Age: {1}", name, age);
Console.WriteLine(message); // 输出 "Name: John, Age: 30"
3.11.4.3.String.Join 方法
String.Join 方法用于将字符串数组连接成一个字符串:
string[] names = { "John", "Jane", "Doe" };
string allNames = String.Join(", ", names);
Console.WriteLine(allNames); // 输出 "John, Jane, Doe"
3.11.5.字符串与字符数组的转换
字符串可以方便地与字符数组进行转换。
3.11.5.1.字符串转字符数组
string message = "Hello";
char[] chars = message.ToCharArray();
3.11.5.2.字符数组转字符串
char[] chars = { 'W', 'o', 'r', 'l', 'd' };
string message = new string(chars);
3.11.6.字符串的不可变性
在 C# 中,字符串是不可变的。这意味着每次修改字符串都会创建一个新的字符串对象,而不会改变原来的字符串。这种设计提高了字符串操作的安全性和效率。
string original = "Hello";
string modified = original.Replace('H', 'J');
Console.WriteLine(original); // 输出 "Hello"
Console.WriteLine(modified); // 输出 "Jello"
3.11.7.StringBuilder
由于字符串的不可变性,在大量拼接字符串时,使用 StringBuilder 可以提高性能。
using System.Text;
StringBuilder sb = new StringBuilder();
sb.Append("Hello");
sb.Append(" ");
sb.Append("World");
string result = sb.ToString();
Console.WriteLine(result); // 输出 "Hello World"
3.11.8.表格展示字符串操作
*操作* | *方法* | *示例代码* |
---|---|---|
连接 | + 或 String.Concat | string fullName = firstName + " " + lastName; |
比较 | ==、Equals、Compare | bool isEqual = str1 == str2; |
查找子字符串 | IndexOf | int index = message.IndexOf(“C#”); |
替换 | Replace | string newMessage = message.Replace(“World”, “C#”); |
拆分 | Split | string[] fruits = data.Split(‘,’); |
截取 | Substring | string subMessage = message.Substring(11, 2); |
格式化 | $、String.Format | $“Name: {name}, Age: {age}”、String.Format(“Name: {0}, Age: {1}”, name, age) |
3.12.静态类和静态方法
静态类(Static Class)和静态方法(Static Method)是面向对象编程中的重要概念。它们允许你创建不依赖于对象实例的方法和属性,而是直接通过类名来访问。
3.12.1.静态类
静态类是一个仅包含静态成员(静态方法、静态属性、静态字段、静态事件等)的类。静态类不能被实例化,也就是说,不能创建静态类的对象。静态类主要用于包含那些与任何对象实例无关的方法或属性。
public static class Math
{
static string j = "sunny老师";
//int i = 1;
public static int Add(int a, int b)
{
return a + b;
}
public static int Multiply(int a, int b)
{
return a * b;
}
}
在这个例子中,Math 是一个静态类,它包含两个静态方法 Add 和 Multiply。由于这是一个静态类,你不能创建 Math 的实例,而是直接通过类名调用它的静态方法:
int sum = Math.Add(5, 3); // 直接调用静态方法
表 2-3 静态类特点
*特点* | *说明* |
---|---|
仅包含静态成员 | 只能包含静态方法、静态属性、静态字段、静态事件等。 |
不能被实例化 | 不能创建静态类的对象。 |
使用 static 关键字定义 | 使用 static 关键字定义静态类。 |
无法包含非静态成员 | 静态类中不允许有非静态成员。 |
3.12.2.静态方法
静态方法是属于类而不是类的任何特定实例的方法。这意味着你不需要创建类的对象就可以调用静态方法。静态方法只能访问静态字段、静态属性、静态方法或类的其他静态成员,不能访问类的非静态成员(除非通过对象实例)。
public class Calculator
{
// 非静态方法
public int AddNumbers(int a, int b)
{
return a + b;
}
// 静态方法
public static int MultiplyNumbers(int a, int b)
{
return a * b;
}
}
在这个例子中,Calculator 类有一个非静态方法 AddNumbers 和一个静态方法 MultiplyNumbers。你可以像下面这样调用这些方法:
// 创建Calculator类的实例并调用非静态方法
Calculator calculator = new Calculator();
int sum = calculator.AddNumbers(5, 3);
// 直接通过类名调用静态方法,不需要创建类的实例
int product = Calculator.MultiplyNumbers(5, 3);
表 2-4 静态方法
*特点* | *说明* |
---|---|
属于类而不是实例 | 静态方法属于类,可以直接通过类名调用。 |
只能访问静态成员 | 静态方法只能访问静态字段、静态属性、静态方法或类的其他静态成员。 |
无需创建类的实例 | 无需创建类的实例即可调用静态方法。 |
3.12.3.静态构造函数
静态类也可以有一个静态构造函数,它会在第一次引用该类的任何静态成员之前自动执行。静态构造函数用于初始化静态字段或执行只需要在类首次加载时执行的操作。静态构造函数不能有访问修饰符(如 public 或 private),并且不能有参数。
public class MyClass
{
// 静态字段
public static int StaticField;
// 静态构造函数
static MyClass()
{
StaticField = 42;
// 这里可以执行其他只需要在类首次加载时执行的初始化操作
}
}
在这个例子中,当 MyClass 首次被引用时(例如,通过访问 MyClass.StaticField),静态构造函数会被执行,将 StaticField 初始化为 42。
表 2-5 静态构造函数
*特点* | *说明* |
---|---|
用于初始化静态字段 | 静态构造函数用于初始化静态字段或执行只需在类首次加载时执行的操作。 |
在首次引用类时执行 | 静态构造函数在首次引用类的任何静态成员时自动执行。 |
无访问修饰符和参数 | 静态构造函数不能有访问修饰符和参数。 |
3.12.4.静态成员与非静态成员的区别
在 C# 中,静态成员和非静态成员有着显著的区别。以下是静态成员与非静态成员的主要区别,使用图标来说明。
表 2-6 静态成员特点
*特点* | *描述* |
---|---|
归属 | 属于类本身 |
依赖性 | 不依赖于对象实例 |
访问方式 | 使用类名访问 |
成员访问 | 只能访问其他静态成员 |
表 2-7 非静态成员
*特点* | *描述* |
---|---|
归属 | 属于对象实例 |
依赖性 | 依赖于对象实例 |
访问方式 | 通过对象访问 |
成员访问 | 可以访问静态和非静态成员 |
3.13.常量和只读变量
在 C# 中,const 和 read-only 是用于定义常量的关键字,但它们的用法和行为有所不同。本文将详细介绍这两个关键字的区别和使用方法。
3.13.1.编译时常量和运行时常量
要搞懂const 和 read-only 的用法,就要先搞懂编译时常量和运行时常量的,我总结在以下表格中:
特性 | 编译时常量(Compile-time Constants) | 运行时常量(Run-time Constants) |
---|---|---|
关键字 | const | readonly |
初始化时机 | 在声明时初始化 | 可以在声明时或构造函数中初始化 |
类型限制 | 只能是基本类型(如整数、浮点数、字符等) | 可以是任何类型,包括对象类型 |
值确定时机 | 编译时确定 | 运行时确定 |
是否可变 | 不可变,编译后不能改变 | 赋值后不可变,但赋值可以在运行时 |
示例 | public const int MaxValue = 100; | public readonly int MaxValue; |
3.13.2.const 关键字
const 关键字用于声明编译时常量。它的值在编译时确定,并且在程序运行时不能更改。const 字段必须在声明时进行初始化。
语法:const 类型 常量名 = 值;
public class Example
{
public const int MaxValue = 100;
}
在上述示例中,MaxValue 是一个常量,它的值为 100,并且在整个程序运行期间不能改变。const常量主要特点如下:
- (1)const 字段隐式为静态,因此它不能与 static 关键字一起使用。
- (2)const 字段只能被赋值为编译时常量,如基本数据类型、枚举或常量表达式。
- (3)const 字段不能包含方法调用、对象创建或其他运行时计算的值。
3.13.3.read-only 关键字
read-only 关键字用于声明运行时常量。它的值可以在构造函数中进行初始化,并且在对象的生命周期内不能更改。
语法:readonly 类型 字段名;
示例:
public class Example
{
public readonly int MaxValue;
public Example(int value)
{
MaxValue = value;
}
}
在上述示例中,MaxValue 是一个只读字段,它的值可以在构造函数中被赋值。read-only只读特点如下:
- read-only 字段的值可以在声明时或在类的构造函数中进行初始化。
- read-only 字段可以是实例字段或静态字段。
- read-only 字段可以包含运行时计算的值,如方法调用或对象创建。
3.13.4.const 和 read-only 的区别
(1)初始化时间: const 字段在编译时初始化,而 read-only 字段可以在运行时初始化(通过构造函数)。
(2)隐式静态: const 字段隐式为静态,而 read-only 字段可以是静态或实例字段。
(3)赋值限制: const 字段只能被赋值为编译时常量,而 read-only 字段可以被赋值为运行时计算的值。
(4)更改时间: const 字段在编译后不能更改,而 read-only 字段在对象的生命周期内不能更改。
3.13.5.实例比较
3.13.5.1.const 示例
在一个数学计算程序中,你可能会有一些数学常数,这些常数的值在编译时已经确定,并且在程序运行期间不会改变。使用 const 关键字可以很好地满足这种需求。
public class MathConstants
{
// 声明数学常数
public const double Pi = 3.141592653589793;
public const double E = 2.718281828459045;
public double CalculateCircleArea(double radius)
{
return Pi * radius * radius;
}
public double CalculateNaturalLogarithm(double value)
{
// 这里使用 E 计算自然对数
return Math.Log(value, E);
}
}
class Program
{
static void Main(string[] args)
{
MathConstants math = new MathConstants();
double area = math.CalculateCircleArea(10);
double logValue = math.CalculateNaturalLogarithm(5);
Console.WriteLine("圆的面积: " + area);
Console.WriteLine("自然对数: " + logValue);
}
}
在这个例子中,Pi 和 E 都是数学常数,它们的值在编译时已经确定并且不会改变,因此使用 const 关键字来声明。
3.13.5.2.read-only 示例
假设你有一个配置类,其中的一些配置项需要在运行时根据具体情况进行初始化,并且在对象的生命周期内不能更改,这时可以使用 read-only 关键字。
public class Configuration
{
// 声明只读字段
public readonly string ConnectionString;
public readonly int MaxConnections;
public Configuration(string connectionString, int maxConnections)
{
// 在构造函数中初始化只读字段
ConnectionString = connectionString;
MaxConnections = maxConnections;
}
public void DisplayConfiguration()
{
Console.WriteLine("Connection String: " + ConnectionString);
Console.WriteLine("Max Connections: " + MaxConnections);
}
}
class Program
{
static void Main(string[] args)
{
// 运行时从配置文件或其他来源获取配置值
string connectionString = "Server=myServer;Database=myDB;User Id=myUsername;Password=myPassword;";
int maxConnections = 100;
Configuration config = new Configuration(connectionString, maxConnections);
config.DisplayConfiguration();
}
}
在这个例子中,ConnectionString 和 MaxConnections 是在运行时从外部来源(例如配置文件)获取的,并在对象创建时初始化。由于这些值在对象的生命周期内不能更改,因此使用 read-only 关键字来声明。
3.14.C# 中的属性
3.14.1.为什么我们需要属性?
在 C# 中,属性(Properties)是一种可以用来保护和管理类内部数据的机制。通过使用 get 和 set 方法,我们可以在读取和写入数据时添加额外的逻辑。这使得我们可以更好地控制数据的访问和修改,从而提高代码的安全性和灵活性。属性允许我们在不直接暴露类的内部数据的情况下,让外部代码安全地访问和修改这些数据。
假设一个类就像一个房子,这个房子里面有一些钱。属性就像是房子的门。通过这扇门,你可以拿到钱(读取数据),也可以放钱进去(写入数据)。get 方法 就像是透过门的窗户看里面有多少钱。set 方法就像是通过门把钱放进去。
在门上你可以装一个密码锁(自定义逻辑),只有输入正确的密码才能打开门(访问或修改数据)。这就确保了只有符合条件的人才能看到或改变房子里面的钱(数据)。
3.14.2.什么是的属性?
C# 中的属性是类、结构或接口的成员,用于提供对私有字段的受控访问。属性类似于字段,但它们是通过 get 和 set 访问器定义的。属性可以包括额外的逻辑来控制数据的读取和写入,而不仅仅是简单的存取。
3.14.3.访问器是什么?
访问器是属性的一部分,用于定义对属性值的访问和修改。C# 中有两种访问器:get 访问器和 set 访问器。get 访问器用于返回属性的值,set 访问器用于设置属性的值。
3.14.4.什么是 Set 访问器?
set 访问器用于分配属性的值。它包含在属性定义中,并定义如何设置私有字段的值。set 访问器可以包含逻辑以验证新值或触发其他操作。value 关键字在 set 访问器中表示正在分配给属性的值。
private int _age;
public int Age
{
get { return _age; }
set
{
if (value >= 0)
{
_age = value;
}
else
{
throw new ArgumentException("Age cannot be negative");
}
}
}
3.14.5.什么是get访问器?
get 访问器用于返回属性的值。它包含在属性定义中,并定义如何获取私有字段的值。get 访问器通常用于返回存储在私有字段中的值。
private int _age;
public int Age
{
get { return _age; }
}
类型 | 描述 |
---|---|
读写属性 | 包含 get 和 set 访问器 |
只读属性 | 仅包含 get 访问器 |
只写属性 | 仅包含 set 访问器(非常少见) |
自动属性 | 不需要显式定义私有字段,编译器自动生成 |
3.14.6.什么是只读属性?
只读属性只有 get 访问器,没有 set 访问器。它们通常用于返回在构造函数或初始化过程中设置的值,并且在类的生命周期内不会更改。
public class Student
{
private int _id;
private string _name;
public int Id
{
get { return _id; }
}
public string Name
{
get { return _name; }
}
public Student(int id, string name)
{
_id = id;
_name = name;
}
}
3.14.7.什么是“只写”属性?
只写属性只有 set 访问器,没有 get 访问器。它们很少使用,通常用于需要外部设置值但不允许外部读取值的情况。
private string _password;
public string Password
{
set { _password = value; }
}
3.14.8.什么是 Read Write 属性?
读写属性包含 get 和 set 访问器,允许对属性的值进行读取和写入。
public class Student
{
private int _id;
private string _name;
public int Id
{
get { return _id; }
set { _id = value; }
}
public string Name
{
get { return _name; }
set { _name = value; }
}
}
3.14.9.访问器的默认辅助功能修饰符
C# 中访问器的默认访问修饰符与属性相同。例如,如果属性是 public 的,那么 get 和 set 访问器也是 public 的。如果需要,可以单独为 get 或 set 访问器指定不同的访问修饰符。
public class Student
{
public int Id { get; private set; }
public string Name { get; private set; }
public Student(int id, string name)
{
Id = id;
Name = name;
}
}
在上述示例中,Id 和 Name 属性的 set 访问器是 private,因此它们只能在类的内部设置。
3.14.10.对称访问器和非对称访问器?
类型 | 描述 |
---|---|
对称访问器 | 指属性的 get 和 set 访问器具有相同的访问修饰符 |
非对称访问器 | 指属性的 get 和 set 访问器具有不同的访问修饰符 |
对称访问器示例:
public int Id { get; set; }
非对称访问器示例:
public int Id { get; private set; }
3.14.11.什么是自动实现属性?
自动实现属性(Auto-Implemented Properties)允许在不显式定义私有字段的情况下声明属性。编译器会自动生成一个私有的匿名字段来存储属性的值。自动属性简化了属性的声明,使代码更加简洁。
public class Student
{
public int Id { get; set; }
public string Name { get; set; }
}
3.14.12.属性有哪些优点?
优点 | 描述 |
---|---|
数据保护 | 通过封装私有字段,防止外部直接访问和修改,提高数据安全性 |
数据验证 | 在设置属性值之前进行验证,确保数据的完整性和有效性 |
代码简化 | 使用自动属性减少代码量,提高代码可读性和可维护性 |
事件触发 | 在属性值变化时触发事件,适用于数据绑定和通知机制 |
3.14.13.应用示例
假设我们有一个用户管理系统,需要管理用户的年龄并确保年龄值在合理范围内,同时在年龄变化时进行一些业务逻辑处理。
public class User
{
private int _age;
public int Age
{
get { return _age; }
set
{
if (value < 0 || value > 120)
{
throw new ArgumentOutOfRangeException("Age must be between 0 and 120.");
}
_age = value;
OnAgeChanged();
}
}
public event Action AgeChanged;
protected virtual void OnAgeChanged()
{
AgeChanged?.Invoke();
}
}
class Program
{
static void Main(string[] args)
{
User user = new User();
user.AgeChanged += () => Console.WriteLine("User's age has changed!");
user.Age = 30; // This will trigger the AgeChanged event
}
}
在这个例子中,我们使用属性来管理用户的年龄,并确保年龄值在合理范围内。当年龄发生变化时,会触发 AgeChanged 事件,从而允许我们执行额外的业务逻辑。
3.15.Checked 和 Unchecked 关键字
在 C# 编程中,checked 和 unchecked 关键字用于控制整数算术运算和类型转换的溢出检查。默认情况下,编译器不会对整数运算执行溢出检查,但可以通过使用 checked 和 unchecked 关键字来显式控制这种行为。
3.15.1.什么是整数溢出?
当整数运算的结果超出数据类型的表示范围时,就会发生溢出。例如,对于一个 byte 类型的变量,其范围是 0 到 255。如果对 byte 类型变量进行运算的结果超过了 255 或低于 0,就会发生溢出。
3.15.2.checked 关键字
checked 关键字用于启用溢出检查。当在 checked 块或表达式中发生溢出时,系统将抛出 OverflowException 异常。
using System;
class Program
{
static void Main()
{
try
{
int max = int.MaxValue;
// 使用 checked 关键字启用溢出检查
int result = checked(max + 1);
Console.WriteLine(result);
}
catch (OverflowException ex)
{
Console.WriteLine("Overflow occurred: " + ex.Message);
}
}
}
在上述示例中,由于 max 的值为 int.MaxValue,即 2147483647,增加 1 会导致溢出,因此会抛出 OverflowException 异常。
3.15.3.unchecked 关键字
unchecked 关键字用于禁用溢出检查。当在 unchecked 块或表达式中发生溢出时,不会抛出异常,结果将会截断为数据类型的最小或最大值。
using System;
class Program
{
static void Main()
{
int max = int.MaxValue;
// 使用 unchecked 关键字禁用溢出检查
int result = unchecked(max + 1);
Console.WriteLine(result); // 输出:-2147483648
}
}
在上述示例中,由于 max 的值为 int.MaxValue,即 2147483647,增加 1 会导致溢出,但由于使用了 unchecked 关键字,结果会被截断为 int 类型的最小值,即 -2147483648。
3.15.4.默认行为
在 C# 中,默认情况下整数运算不进行溢出检查,除非在编译时使用 /checked 编译选项或在代码中使用 checked 关键字。
3.15.5.使用场景
checked: 当你希望在整数运算中检测到溢出并处理时使用。例如,金融计算、科学计算等需要精确结果的场景。
unchecked: 当你希望忽略溢出并允许结果截断时使用。例如,在性能关键的代码中,或当你确定溢出不会引起问题时。
3.15.6.代码块中的 checked 和 unchecked
你可以在代码块中使用 checked 和 unchecked 关键字来启用或禁用整个代码块中的溢出检查。
using System;
class Program
{
static void Main()
{
try
{
checked
{
int max = int.MaxValue;
int result = max + 1;
Console.WriteLine(result);
}
}
catch (OverflowException ex)
{
Console.WriteLine("Overflow occurred: " + ex.Message);
}
unchecked
{
int max = int.MaxValue;
int result = max + 1;
Console.WriteLine(result); // 输出:-2147483648
}
}
}
在上述示例中,第一个 checked 块启用了溢出检查,并在溢出时抛出异常。第二个 unchecked 块禁用了溢出检查,并允许结果截断。
3.16.栈和堆内存
在 C# 中,内存管理是编程中的一个关键概念。理解堆(Heap)和栈(Stack)如何工作,可以帮助开发者编写更高效的代码,并避免一些常见的性能问题。本文将详细介绍堆和栈的基本概念、它们的区别以及它们在 C# 编程中的使用。
3.16.1.英语单词辨析
These two concepts are easily confused, let’s understand them from their original meaning in English.
heap of things is usually untidy, and often has the shape of a hill or mound. Now, the house is a heap of rubble. A stack is usually tidy, and often consists of flat objects placed directly on top of each other. …a neat stack of dishes. A pile of things can be tidy or untidy. …a neat pile of clothes.
这两个概念很容易混淆,我们从英文的原本的意思去理解。
heap 通常指杂乱的、呈小山状的一堆东西,如:Now, the house is a heap of rubble(现在,房子成了一堆瓦砾)。stack通常是整齐的一叠,指扁平物体叠放起来,如:a neat stack of dishes(整齐的一叠盘子)。pile 既可指整齐的一叠,也可指杂乱的一堆,如:a neat pile of clothes(整齐的一叠衣服)。
3.16.2.栈(Stack)
栈是一种基于 LIFO(Last In, First Out,即后进先出)原则的内存管理结构,用于存储方法调用期间的局部变量和方法参数。
想象一下你在玩积木塔游戏,每次你都把新的积木放在最上面,然后从最上面的积木开始取积木。这就是栈的工作原理:后进先出。栈用于存储方法内部的局部变量和参数。
特点 | 描述 |
---|---|
内存管理 | 自动管理(方法调用结束时自动释放) |
存储内容 | 局部变量和方法参数 |
访问速度 | 快速(因为是顺序访问) |
空间大小 | 较小(通常几兆字节) |
数据结构 | 后进先出(LIFO) |
生命周期 | 方法调用结束时结束 |
典型示例 | 局部变量、方法参数、值类型(如 int、double) |
void ExampleMethod()
{
int a = 10; // 'a' 存储在栈中
int b = 20; // 'b' 存储在栈中
int c = a + b; // 'c' 存储在栈中
}
在这个示例中,变量 a、b 和 c 都存储在栈中。当 ExampleMethod 方法执行完毕时,这些变量占用的栈内存会自动释放。
3.16.3.堆(Heap)
堆是一种用于存储动态分配的内存的结构,如对象实例。堆基于随机存取原则,允许在任意顺序中分配和释放内存。
特点 | 描述 |
---|---|
内存管理 | 手动管理(由垃圾收集器管理) |
存储内容 | 动态分配的对象实例和引用类型 |
访问速度 | 相对较慢(因为是随机访问) |
空间大小 | 较大(可以达到几百兆字节甚至更多) |
数据结构 | 随机访问 |
生命周期 | 直到垃圾收集器回收 |
典型示例 | 类实例、数组、引用类型(如 object、string) |
class ExampleClass
{
public int X;
public int Y;
}
void ExampleMethod()
{
ExampleClass obj = new ExampleClass(); // 'obj' 存储在堆中
obj.X = 10;
obj.Y = 20;
}
在这个示例中,ExampleClass 的实例 obj 存储在堆中。当 ExampleMethod 方法执行完毕时,obj 所占用的内存不会立即释放,而是等待垃圾收集器来回收。
3.16.4.堆和栈的区别
特性 | 栈(Stack) | 堆(Heap) |
---|---|---|
内存管理 | 自动管理(方法调用结束时自动释放) | 手动管理(由垃圾收集器管理) |
存储内容 | 局部变量和方法参数 | 动态分配的对象实例和引用类型 |
访问速度 | 快速 | 相对较慢 |
空间大小 | 较小 | 较大 |
数据结构 | 后进先出(LIFO) | 随机访问 |
生命周期 | 方法调用结束时结束 | 直到垃圾收集器回收 |
典型示例 | 局部变量、方法参数、值类型(如 int、double) | 类实例、数组、引用类型(如 object、string) |
3.16.5.使用场景
栈:适用于生命周期短的局部变量和方法参数,例如在方法内部声明的简单变量。
void StackExample()
{
int a = 10; // 存储在栈中
int b = 20; // 存储在栈中
int c = a + b; // 存储在栈中
}
堆:适用于生命周期较长的对象实例,例如类的实例和动态分配的内存。
class Person
{
public string Name;
public int Age;
}
void HeapExample()
{
Person person = new Person(); // 存储在堆中
person.Name = "Alice";
person.Age = 30;
}
3.16.6.垃圾收集(Garbage Collection)
垃圾收集器(GC)是 .NET 框架中的一个自动内存管理系统,用于释放不再使用的对象所占用的内存。GC 会定期扫描堆中的对象,标记和回收不再使用的对象,防止内存泄漏。
阶段 | 描述 |
---|---|
标记阶段 | GC 会标记所有活动的对象(仍在使用的对象)。 |
清除阶段 | GC 会清除未标记的对象(不再使用的对象),释放它们占用的内存。 |
压缩阶段 | GC 会压缩堆中的内存,减少碎片化,提高内存使用效率。 |
3.16.7.总结
在 C# 中,理解堆和栈的工作原理是编写高效代码的关键。栈用于存储局部变量和方法参数,具有快速访问和自动管理的特点;而堆用于存储动态分配的对象,具有较大的存储空间和需要垃圾收集器管理的特点。
3.17.装包与拆包(装箱和拆箱)
装包是把数值放进盒子里变成对象,拆包是把对象里的数值取出来。
本文将通过图表详细说明装包和拆包的基本概念、工作原理、使用示例及其优缺点。
3.17.1.什么是装包(Boxing)?
装包是指将一个值类型转换为引用类型的过程。在 C# 中,所有的值类型(如 int、double、struct 等)都可以被装包为 object 类型或实现接口的类型。装包会在堆上创建一个对象,并将值类型的值复制到该对象中。
int number = 123;
object boxedNumber = number; // 装包过程
3.17.2.什么是拆包(Unboxing)?
拆包是指将一个装包后的引用类型转换回值类型的过程。拆包需要显式转换,并且只有在确认引用类型确实是装包后的值类型时才可以进行,否则会引发异常。
object boxedNumber = 123; // 装包
int number = (int)boxedNumber; // 拆包过程
3.17.3.装包和拆包的工作原理
int number = 123; // 值类型,存储在栈上
object boxedNumber = number; // 装包,值复制到堆上的新对象
int unboxedNumber = (int)boxedNumber; // 拆包,值复制回栈上的值类型变量
3.17.3.1.拆包原理
步骤 | 描述 | 内存状态 |
---|---|---|
初始状态 | 引用类型 boxedNumber 存储在堆上 | 堆:[ boxedNumber: 123 ] |
拆包操作 | boxedNumber 被拆包为 unboxedNumber,值复制到栈上 | 栈:[ unboxedNumber: 123 ]堆:[ boxedNumber: 123 ] |
最终状态 | 栈上的 unboxedNumber 和堆上的 boxedNumber 各自存储值 | 栈:[ unboxedNumber: 123 ]堆:[ boxedNumber: 123 ] |
3.17.3.2.装包原理
步骤 | 描述 | 内存状态 |
---|---|---|
初始状态 | 值类型 number 存储在栈上 | 栈:[ number: 123 ] |
装包操作 | number 被装包为 boxedNumber,值复制到堆上 | 栈:[ number: 123 ]堆:[ boxedNumber: 123 ] |
最终状态 | 栈上的 number 和堆上的 boxedNumber 各自存储值 | 栈:[ number: 123 ]堆:[ boxedNumber: 123 ] |
时
3.17.4.装包和拆包的优缺点
优点 | 描述 |
---|---|
灵活性 | 允许在需要引用类型的上下文中使用值类型,例如在集合中存储不同类型的数据。 |
接口实现 | 值类型可以实现接口,并在装包后作为接口类型处理。 |
缺点 | 描述 |
---|---|
性能开销 | 装包和拆包涉及值的复制和堆的分配,可能会导致性能下降。 |
内存开销 | 频繁的装包和拆包可能导致大量的堆内存分配和垃圾回收。 |
3.17.5.装包和拆包在集合中的应用
在使用旧版集合(如 ArrayList)时,装包和拆包非常常见。以下示例展示了如何在 ArrayList 中存储和检索值类型数据:
using System;
using System.Collections;
class Program
{
static void Main()
{
ArrayList list = new ArrayList();
int number = 123;
list.Add(number); // 装包
int retrievedNumber = (int)list[0]; // 拆包
Console.WriteLine(retrievedNumber); // 输出: 123
}
}
3.17.6.避免不必要的装包和拆包
为了提高性能,尽量避免不必要的装包和拆包。可以使用泛型集合(如 List)来替代旧版非泛型集合(如 ArrayList),从而避免装包和拆包。使用泛型集合的示例
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
List<int> list = new List<int>();
int number = 123;
list.Add(number); // 没有装包
int retrievedNumber = list[0]; // 没有拆包
Console.WriteLine(retrievedNumber); // 输出: 123
}
}
在上述示例中,List 是一个泛型集合,不需要装包和拆包,提高了性能。
3.17.7.总结
装包和拆包是 C# 中处理值类型和引用类型转换的重要概念。虽然它们提供了灵活性,但频繁使用可能导致性能问题。因此,在编写代码时,应该尽量使用泛型集合和其他方法来避免不必要的装包和拆包操作,从而提高代码的效率和性能。
4.面向对象编程(OOPs)
4.1.面向对象编程简介
在这篇文章中,我们将学习 C# 中的面向对象编程(OOP),其核心原理包括类、对象、继承、多态、抽象和封装。
Object-Oriented Programming,直译是以对象为目标的编程,在中文中被翻译成“面向对象编程”。
4.1.1.类和对象
类是定义对象的模板。类表示对象的属性和行为。它们以成员变量表示属性,以方法表示行为。类包含了对象所需的所有特征。
对象是类的实例。对象通过其属性来保存状态,并通过其方法来展示行为。
// 定义一个简单的类
public class Book
{
// 数据成员
public string title;
public string author;
// 方法
public void PrintDetails()
{
Console.WriteLine($"Title: {title}, Author: {author}");
}
}
// 使用类
public class Program
{
public static void Main()
{
// 创建对象
Book myBook = new Book();
myBook.title = "1984";
myBook.author = "George Orwell";
// 调用方法
myBook.PrintDetails(); // 输出: Title: 1984, Author: George Orwell
}
}
在上面的示例中,我们创建了一个名为 Book 的类,该类包含两个公共数据成员 title 和 author 以及一个方法 PrintDetails()。在 Program 类中,我们创建了 Book 类的对象,并访问其成员。
4.1.2.封装
封装是一种将数据和代码绑定在一起,并限制对某些数据的访问的机制。在 C# 中,封装是通过使用访问修饰符(如 private、public、protected 和 internal)实现的。
public class Person
{
// 私有数据成员
private string name;
// 公共属性
public string Name
{
get { return name; }
set { name = value; }
}
}
public class Program
{
public static void Main()
{
Person person = new Person();
person.Name = "Alice";
Console.WriteLine(person.Name); // 输出: Alice
}
}
在这个简短的示例中,name 数据成员被声明为 private,并通过公共属性 Name 来访问和修改。这展示了如何使用属性来实现封装。
4.1.3.继承
继承是一种获取现有类的特性并创建新类的机制。继承通过现有类创建新类,现有类称为基类或父类,新类称为派生类或子类。
// 基类
public class Animal
{
public string Species { get; set; }
public void MakeSound()
{
Console.WriteLine("Some generic animal sound");
}
}
// 派生类
public class Dog : Animal
{
public string Breed { get; set; }
public void Bark()
{
Console.WriteLine("Woof! Woof!");
}
}
public class Program
{
public static void Main()
{
Dog myDog = new Dog
{
Species = "Canine",
Breed = "Golden Retriever"
};
myDog.MakeSound(); // 输出: Some generic animal sound
myDog.Bark(); // 输出: Woof! Woof!
Console.WriteLine($"Species: {myDog.Species}, Breed: {myDog.Breed}"); // 输出: Species: Canine, Breed: Golden Retriever
}
}
在这个示例中,Dog 类继承了 Animal 类,因此 Dog 类可以访问 Animal 类的成员,并且我们可以在 Dog 类中添加新的成员和方法。
4.1.4.多态
多态是一种允许同一接口用于不同数据类型的机制。多态有两种类型:编译时多态和运行时多态。
4.1.4.1.编译时多态(方法重载)
方法重载是同一类中具有相同名称但参数不同的多个方法。
public class Printer
{
public void Print(string message)
{
Console.WriteLine(message);
}
public void Print(int number)
{
Console.WriteLine(number);
}
public void Print(double number)
{
Console.WriteLine(number);
}
}
public class Program
{
public static void Main()
{
Printer printer = new Printer();
printer.Print("Hello, World!"); // 输出: Hello, World!
printer.Print(100); // 输出: 100
printer.Print(99.99); // 输出: 99.99
}
}
在这个示例中,Printer 类中有三个名为 Print 的方法,但它们接受不同类型的参数(字符串、整数和双精度数)。这展示了编译时多态,即方法重载。
4.1.4.2.运行时多态(方法重写)
方法重写是派生类通过重新定义基类的方法来实现多态。
public class Animal
{
public virtual void MakeSound()
{
Console.WriteLine("Some generic animal sound");
}
}
public class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine("Bark");
}
}
public class Cat : Animal
{
public override void MakeSound()
{
Console.WriteLine("Meow");
}
}
public class Program
{
public static void Main()
{
Animal myDog = new Dog();
Animal myCat = new Cat();
myDog.MakeSound(); // 输出: Bark
myCat.MakeSound(); // 输出: Meow
}
}
在这个示例中,Dog 和 Cat 类继承了 Animal 类,并重写了 MakeSound 方法。这展示了运行时多态,即方法重写。在运行时,根据对象的实际类型调用相应的方法。
4.1.5.抽象
抽象是通过抽象类和接口来实现的。抽象类和接口不能被实例化,只能被继承或实现。抽象类可以包含抽象方法和具体方法,而接口只能包含抽象方法。
4.1.5.1.抽象类
// 抽象类
public abstract class Animal
{
// 抽象方法
public abstract void MakeSound();
// 具体方法
public void Sleep()
{
Console.WriteLine("Sleeping...");
}
}
// 派生类
public class Dog : Animal
{
// 实现抽象方法
public override void MakeSound()
{
Console.WriteLine("Bark");
}
}
public class Cat : Animal
{
// 实现抽象方法
public override void MakeSound()
{
Console.WriteLine("Meow");
}
}
public class Program
{
public static void Main()
{
Animal myDog = new Dog();
Animal myCat = new Cat();
myDog.MakeSound(); // 输出: Bark
myDog.Sleep(); // 输出: Sleeping...
myCat.MakeSound(); // 输出: Meow
myCat.Sleep(); // 输出: Sleeping...
}
}
在这个示例中,Animal 是一个抽象类,包含一个抽象方法 MakeSound 和一个具体方法 Sleep。Dog 和 Cat 类继承了 Animal 类,并实现了 MakeSound 方法。抽象类的具体方法可以直接在派生类对象上调用,而抽象方法必须在派生类中实现。
4.1.5.2.接口
// 接口
public interface IMovable
{
void Move();
}
// 实现接口的类
public class Car : IMovable
{
public void Move()
{
Console.WriteLine("The car is moving");
}
}
public class Person : IMovable
{
public void Move()
{
Console.WriteLine("The person is walking");
}
}
public class Program
{
public static void Main()
{
IMovable myCar = new Car();
IMovable myPerson = new Person();
myCar.Move(); // 输出: The car is moving
myPerson.Move(); // 输出: The person is walking
}
}
在这个示例中,IMovable 是一个接口,包含一个抽象方法 Move。Car 和 Person 类实现了 IMovable 接口,并提供了 Move 方法的具体实现。接口确保了实现它的类必须实现所有接口方法,从而提供了一种实现多态的方式。
4.2.类和对象
类和对象是 C# 编程语言的核心概念。类是一种蓝图或模板,用于创建对象,而对象是类的实例。对象表示真实世界中的实体。类定义了对象的属性和行为。属性是数据成员,行为是成员方法。
首先,我们将创建一个名为 Student 的类,然后我们将创建该类的对象来访问其成员。
4.2.1.类的定义
public class Student
{
// 数据成员
public string Name;
public int Age;
public string Gender;
// 成员方法
public void GetDetails()
{
Console.WriteLine("Name: " + Name);
Console.WriteLine("Age: " + Age);
Console.WriteLine("Gender: " + Gender);
}
}
4.2.2.对象的创建
class Program
{
static void Main(string[] args)
{
// 创建Student类的对象
Student student1 = new Student();
student1.Name = "John";
student1.Age = 20;
student1.Gender = "Male";
// 访问对象的成员
student1.GetDetails();
// 创建另一个Student类的对象
Student student2 = new Student();
student2.Name = "Emma";
student2.Age = 22;
student2.Gender = "Female";
// 访问对象的成员
student2.GetDetails();
}
}
在上面的示例中,我们创建了一个名为 Student 的类,该类包含三个数据成员 Name、Age 和 Gender 以及一个成员方法 GetDetails()。然后,我们在 Program 类中创建了 Student 类的两个对象 student1 和 student2,并访问它们的成员。
4.2.3.对象初始化
在 C# 中,可以在创建对象时初始化其数据成员。
class Program
{
static void Main(string[] args)
{
// 使用对象初始化器创建并初始化对象
Student student1 = new Student { Name = "John", Age = 20, Gender = "Male" };
student1.GetDetails();
// 使用对象初始化器创建并初始化另一个对象
Student student2 = new Student { Name = "Emma", Age = 22, Gender = "Female" };
student2.GetDetails();
}
}
在上面的示例中,我们使用对象初始化器在创建对象时初始化其数据成员。这样可以在一行代码中完成对象的创建和初始化。
4.2.4.构造函数
构造函数是一种特殊的方法,用于在创建对象时初始化对象的状态。构造函数的名称与类名相同,它没有返回类型。让我们通过示例来理解构造函数。
public class Student
{
// 数据成员
public string Name;
public int Age;
public string Gender;
// 构造函数
public Student(string name, int age, string gender)
{
Name = name;
Age = age;
Gender = gender;
}
// 成员方法
public void GetDetails()
{
Console.WriteLine("Name: " + Name);
Console.WriteLine("Age: " + Age);
Console.WriteLine("Gender: " + Gender);
}
}
class Program
{
static void Main(string[] args)
{
// 创建对象时调用构造函数
Student student1 = new Student("John", 20, "Male");
student1.GetDetails();
// 创建另一个对象时调用构造函数
Student student2 = new Student("Emma", 22, "Female");
student2.GetDetails();
}
}
在上面的示例中,我们在 Student 类中定义了一个构造函数,该构造函数接受三个参数并初始化数据成员。然后,在创建 Student 对象时,我们传递参数来调用构造函数,从而初始化对象的状态。
4.2.5.C#析构函数
析构函数是一种特殊的方法,用于在对象被销毁时执行清理操作。析构函数的名称与类名相同,但前面带有波浪号(~)。析构函数没有参数和返回类型。让我们通过示例来理解析构函数。
public class Student
{
// 数据成员
public string Name;
public int Age;
public string Gender;
// 构造函数
public Student(string name, int age, string gender)
{
Name = name;
Age = age;
Gender = gender;
Console.WriteLine("Constructor called for " + Name);
}
// 析构函数
~Student()
{
Console.WriteLine("Destructor called for " + Name);
}
// 成员方法
public void GetDetails()
{
Console.WriteLine("Name: " + Name);
Console.WriteLine("Age: " + Age);
Console.WriteLine("Gender: " + Gender);
}
}
class Program
{
static void Main(string[] args)
{
// 创建对象时调用构造函数
Student student1 = new Student("John", 20, "Male");
student1.GetDetails();
// 创建另一个对象时调用构造函数
Student student2 = new Student("Emma", 22, "Female");
student2.GetDetails();
}
}
在上面的示例中,我们在 Student 类中定义了一个析构函数,用于在对象被销毁时执行清理操作。析构函数在对象超出其作用域或程序终止时自动调用。在程序执行结束时,会调用析构函数来销毁对象,并释放资源。
4.3.构造函数
4.3.1.什么是构造函数?
构造函数是负责初始化该类中的变量的一种特殊方法。
构造函数方法的名称与它所在的类的名称完全相同,且无法更改名称。如果类名是 Employee,则构造函数的名称将为 Employee,如果类名为 Student,则构造函数称也将为 Student。
4.3.2.构造函数的作用
作用 | 解释 |
---|---|
初始化对象的状态 | 构造函数用于设置对象的初始状态,即为对象的成员变量赋初始值。 |
保证对象的有效状态 | 构造函数可以确保对象在创建时被赋予有效的值,从而避免使用未初始化的对象。 |
提供默认值 | 如果没有提供构造函数,编译器会自动提供一个默认构造函数,该构造函数没有参数并将成员变量初始化为其默认值。 |
支持依赖注入 | 构造函数可以用于依赖注入,通过构造函数将依赖项传递给对象,从而实现松耦合和可测试的代码。 |
4.3.3.隐式定义构造函数
为了更好地理解,我们创建了一个test类,它显示了编译前后的代码:
//编译前:
class Test
{
int i;
string s;
bool b;
}
//编译后:
class Test
{
int i;
string s;
bool b;
public Test()
{
i = 0; s = null; b = false;
}
}
我们可以看到test类。在编译前并没有构造函数,但编译后却有一个test构造函数,这说明如果我们没有在类中显示的声明构造函数,则由编译器隐式的提供。换句话说,每一个类中都存在一个构造函数,无论它是显示的提供还是由编译器隐式的提供。
注意:
(1)隐式定义的构造函数是无参数的,这些构造函数也称为默认构造函数;
(2)隐式定义的构造函数是公共的;
(3)可以在类下定义一个构造函数,如果我们定义它,我们可以称它为显式构造函数,显式构造函数可以是无参数的,也可以是参数化的。
4.3.4.显式的定义构造函数
我们还可以在 C# 中显式定义构造函数。以下是显式构造函数语法。下面代码包括两个构造函数,一个是有参构造,一个是无参构造。
public class Test
{
public int i;
public Test()
{
i = 99 ;
Console.WriteLine("调用了无参构造函数");
}
public Test(int i)
{
this.i = i ;
Console.WriteLine("调用了有参构造函数");
}
}
4.3.5.调用构造函数
当我们创建一个类的时候,构造函数就被调用。
Test test = new Test();//调用无参构造函数。
Test test1 = new Test(3);//调用有参构造函数
输出结果
4.3.6.静态构造函数
在 C# 中,也可以将构造函数创建为静态构造函数,当我们这样做时,它称为静态构造函数。在静态构造函数中,不能使用任何访问说明符,如 public、private 和 protected。我们还是以test为例来看见来看怎么创建一个静态的构造函数。
//显示的调用
public class test
{
static test()
{
}
}
上述例子是显示的创建静态构造函数,那是否可以由编译器隐式的提供静态构造函数呢?可以,当类中有静态成员时,即使我们没有显示的定义静态构造函数,编译器也会隐式地提供一个静态构造函数。还是上面的例子,我们在test类中添加一个静态成员变量j, 在这个内容,虽然我们没有显示的提供一个静态构造函数,但是编译器会隐式帮我们提供一个静态构造函数。
public class Test
{
public int i;
public static int j;
public Test()
{
i = 99 ;
Console.WriteLine("调用了无参构造函数");
}
public Test(int i)
{
this.i = i ;
Console.WriteLine("调用了有参构造函数");
}
}
静态构造函数永远是在类中第一个被执行的代码,所以我们在静态构造函数中无法传递参数。换句话说,静态构造函数都是无参构造函数,如果我们传递参数,编译器会报错。
注意:
- (1)一个类中只能有一个静态构造函数。
- (2)它不能被显式调用,它总是被隐式调用。静态构造函数应不带任何参数。
- (3)它只能访问类的静态成员。
- (4)静态构造函数定义中不应有任何访问说明符。静态构造函数只调用一次,即在类加载时。
4.3.7.私有构造函数?
在 C# 中,也可以将构造函数创建为私有构造函数。当构造函数被私有访问修饰符修饰时,我们称为私有构造函数。私有构造函数只能被内部的成员调用,所以我们通过内部的CreateInstance方法调用私有构造函数。
public class MyClass
{
// 类的某个属性
public int Value { get; private set; }
// 私有的有参构造函数
private MyClass(int value)
{
Value = value;
Console.WriteLine($"MyClass instance created with value: {value}");
}
// 公共的静态创建方法
public static MyClass CreateInstance(int value)
{
return new MyClass(value); // 在类内部调用私有的构造函数
}
}
当我们从外部调用时,编译器会报错。
那是不是有私有构造函数时,我们就不能够从外部创建类的实例呢?答案是否定的,当内中还存在公有构造函数时,我们仍然可以创建类的实例,只不过创建类的实例是通过公有构造函数。
public class MyClass
{
// 类的某个属性
public int Value { get; private set; }
// 私有的有参构造函数
private MyClass(int value)1500.
{
Value = value;
Console.WriteLine($"MyClass instance created with value: {value}");
}
// 公共的静态创建方法
public static MyClass CreateInstance(int value)
{
return new MyClass(value); // 在类内部调用私有的构造函数
}
public MyClass()
{
Console.WriteLine("通过共有构造函数创建。");
}
}
运行结果
4.3.8.普通构造函数和静态构造函数区别
*特性* | *静态构造函数* | *非静态构造函数* |
---|---|---|
定义 | 用于初始化类级别的静态成员。 | 用于初始化类实例级别的非静态成员。 |
关键字 | 无关键字,仅使用类名和静态修饰符。 | 无关键字,仅使用类名。 |
调用时机 | 当类的任何静态成员被访问或类的实例被创建时,首次触发。 | 每次创建类的新实例时触发。 |
参数 | 不接受任何参数。 | 可以接受参数。 |
多个构造函数 | 每个类最多一个静态构造函数。 | 一个类可以有多个重载的非静态构造函数。 |
可见性 | 不能显式调用,自动调用。 | 可显式调用。 |
使用场景 | 主要用于初始化静态字段或执行仅需一次的操作。 | 用于初始化实例字段或执行每次实例化时的操作。 |
4.3.9.公有构造函数和私有构造函数的区别
*特性* | *公有构造函数* | *私有构造函数* |
---|---|---|
定义 | 用于允许外部代码创建类的实例。 | 限制外部代码创建类的实例。 |
关键字 | public | private |
可见性 | 在类的外部可见,允许实例化。 | 在类的外部不可见,禁止实例化。 |
使用场景 | 普通类的实例化。 | 单例模式或禁止实例化。 |
访问权限 | 任何类都可以访问并调用。 | 仅类自身可以访问并调用。 |
4.4.垃圾回收器(GC)
4.4.1.什么是垃圾回收器(GC)?
GC的全称是Garbage Collector,翻译为垃圾回收器。
垃圾回收器(GC)是.NET框架中的一部分,负责自动管理和回收不再使用的内存资源。想象一下,你是一个房东,有很多房间租给不同的租客。当租客搬出去后,你需要清理房间,以便新的租客可以入住。GC就像一个自动清洁工,会自动检测哪些房间空了,并进行清理。
4.4.2.GC的工作流程
4.4.2.1.内存分配
当你在代码中创建一个新对象时,.NET运行时会在内存中为这个对象分配一个内存。这个过程很快,因为GC使用了一种叫做“托管堆”(Managed Heap)的内存管理方式。托管堆就像一个大仓库,所有的新对象都放在里面。
4.4.2.2.标记阶段(Marking Phase)
GC的第一步是“标记阶段”。就像清洁工检查每个房间,看哪些房间是空的,哪些房间还有租客。GC会遍历所有的活动引用,找到所有仍然被使用的对象,并标记它们。这个过程称为“标记”,因为GC在内存中标记这些对象为“活的”。
活动对象:还在使用的对象,比如你在代码中还引用的变量。
非活动对象:不再使用的对象,比如你不再引用的变量。
4.4.2.3.清除阶段(Sweeping Phase)
接下来是“清除阶段”。清洁工把所有没被标记的房间清扫干净。GC会遍历托管堆,清理所有没有被标记的对象,释放它们占用的内存。这样,这些内存就可以用来存放新的对象。
4.4.2.4.压缩阶段(Compaction Phase)
清理完房间后,清洁工还要把房间整理得整齐一些。GC会把存活的对象搬到堆的一端,消除内存中的“空洞”(内存碎片)。这样可以使新的对象分配更加高效。这个过程叫做“压缩”。
4.4.3.什么时候会触发GC?
GC不会在每次对象变得不可达时立即运行。它会根据需要在后台自动运行,通常在以下情况触发:
- 内存不足:当系统内存不足时,GC会运行以释放更多内存。
- 显式调用:你可以在代码中显式调用GC.Collect()方法,但一般不推荐这样做,因为GC有自己的优化策略。
- 托管堆增长:当托管堆的大小超过一定阈值时,GC会自动运行。
4.4.4.GC的代(Generation)
GC将托管堆分为三代(Generations):
第0代(Generation 0):用于存放新创建的对象。GC运行时首先检查这一代,因为大多数对象很快会变得不可达。
第1代(Generation 1):存放从第0代晋升的对象。这些对象的存活时间稍长。
第2代(Generation 2):存放存活时间最长的对象。从第1代晋升的对象存放在这里。
每次GC运行时,通常首先清理第0代,因为清理这代的开销最小。如果需要更多内存,GC会继续清理第1代和第2代。
通过一个简单的示例代码来说明GC的代和对象晋升的过程:
using System;
class MyClass
{
public MyClass()
{
Console.WriteLine("对象被创建");
}
~MyClass()
{
Console.WriteLine("对象被销毁");
}
}
class Program
{
static void Main(string[] args)
{
// 创建第0代对象
Console.WriteLine("创建第0代对象");
for (int i = 0; i < 10; i++)
{
MyClass obj = new MyClass();
}
// 强制GC回收,回收第0代对象
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine("第0代对象已回收");
// 创建并保持第1代对象
Console.WriteLine("创建第1代对象");
MyClass[] gen1Objects = new MyClass[10];
for (int i = 0; i < 10; i++)
{
gen1Objects[i] = new MyClass();
}
// 再次强制GC回收,第1代对象晋升
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine("第1代对象已晋升");
// 创建并保持第2代对象
Console.WriteLine("创建第2代对象");
MyClass[] gen2Objects = new MyClass[10];
for (int i = 0; i < 10; i++)
{
gen2Objects[i] = new MyClass();
}
// 再次强制GC回收,第2代对象晋升
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine("第2代对象已晋升");
// 释放所有对象
gen1Objects = null;
gen2Objects = null;
// 最后一次强制GC回收,回收所有对象
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine("所有对象已回收");
}
}
4.4.5.小贴士
(1)不要显式调用GC:让GC自动运行,因为它有自己的优化策略。
(2)实现IDisposable接口:如果你的类使用了非托管资源,最好实现IDisposable接口,并在Dispose方法中释放这些资源。
4.5.析构函数
4.5.1.什么是析构函数
析构函数(Destructor)是一个类的特殊方法,当一个对象的生命周期结束时,系统会自动调用它来释放资源。析构函数的主要作用是执行清理操作,例如释放非托管资源(如文件句柄、数据库连接等)。
注意:析构函数是由.net平台自动调用的,调用时机不确定;同时不允许手动调用。
using System;
class MyClass
{
// 构造函数
public MyClass()
{
Console.WriteLine("对象被创建");
}
// 析构函数
~MyClass()
{
Console.WriteLine("对象被销毁");
}
}
class Program
{
static void Main(string[] args)
{
MyClass obj = new MyClass();
}
}
4.5.2.析构函数的特点
-
命名和语法:析构函数的名称与类名相同,前面加上波浪号(~),且没有返回类型和参数。
-
调用时机:析构函数在垃圾回收器(Garbage Collector,GC)确定对象不再被引用时调用。
-
不确定性:析构函数的调用时间是不确定的,只有在GC运行时才会被调用。
-
手动调用:析构函数不能被手动调用,必须由GC自动调用。
-
继承关系:基类的析构函数会在派生类析构函数之后调用。
class ClassName
{
// 构造函数
public ClassName()
{
// 初始化操作
}
// 析构函数
~ClassName()
{
// 清理操作
}
}
4.5.3.应用场景
(1)释放非托管资源,例如文件句柄、数据库连接、网络连接等。
(2)执行对象被销毁前的最后操作,例如日志记录、通知其他对象等。
using System;
using System.IO;
class FileHandler
{
// 文件流,用于处理文件的非托管理资源
private FileStream fileStream;
// 构造函数,打开一个文件
public FileHandler(string filePath)
{
fileStream = new FileStream(filePath, FileMode.OpenOrCreate);
Console.WriteLine("文件已打开");
}
// 析构函数,释放文件句柄
~FileHandler()
{
// 检查文件流是否已被关闭
if (fileStream != null)
{
fileStream.Close();
Console.WriteLine("文件流已关闭");
}
}
}
class Program
{
static void Main(string[] args)
{
// 创建FileHandler对象
FileHandler handler = new FileHandler("example.txt");
// 强制垃圾回收,调用析构函数
handler = null;
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine("程序结束");
}
}
4.5.4.使用要点
必要性:仅当类中使用了非托管资源时,才需要定义析构函数。
效率问题:析构函数会影响垃圾回收的性能,因此不要滥用析构函数。
Dispose模式:如果需要明确释放资源,可以使用IDisposable接口和Dispose方法,结合析构函数来实现资源释放。
4.5.5.注意事项
避免长时间操作:析构函数中避免长时间操作,如网络请求、数据库查询等。
异常处理:析构函数中不要抛出异常,否则可能导致程序崩溃。
调用基类析构函数:在派生类的析构函数中,确保调用基类的析构函数来释放基类中的资源。
4.5.6.Dispose模式实现
using System;
class MyClass : IDisposable
{
// 构造函数
public MyClass()
{
// 初始化操作
}
// 实现Dispose方法
public void Dispose()
{
// 释放托管资源
Dispose(true);
// 阻止垃圾回收器调用析构函数
GC.SuppressFinalize(this);
}
// 受保护的Dispose方法,供析构函数和Dispose方法调用
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
// 释放托管资源
}
// 释放非托管资源
}
// 析构函数
~MyClass()
{
// 调用Dispose方法,释放非托管资源
Dispose(false);
}
}
4.5.7.析构函数和Dispose方法的对比
*特性* | *析构函数* | *Dispose方法* |
---|---|---|
调用时机 | 由垃圾回收器自动调用 | 由程序员手动调用 |
调用频率 | 不确定,仅在GC运行时调用 | 根据需要多次调用 |
参数和返回类型 | 无参数、无返回类型 | 无参数、无返回类型 |
用途 | 释放非托管资源 | 释放托管和非托管资源 |
执行开销 | 较高,对GC性能有影响 | 较低,程序员控制 |
例外处理 | 不应抛出例外,否则程序崩溃 | 可以安全处理例外 |
4.6.Dispose 方法详解
4.6.1.Dispose 方法简介
Dispose 方法是 IDisposable 接口的一部分,用于显式释放托管资源和非托管资源。实现 Dispose 方法可以确保资源在不再需要时被及时释放,防止资源泄漏和提高应用程序的性能。
4.6.2.Dispose 方法的实现步骤
实现 IDisposable 接口:定义一个类并实现 IDisposable 接口。
定义 Dispose 方法:在 Dispose 方法中释放托管资源和非托管资源。
多次调用安全:确保 Dispose 方法可以多次调用而不会抛出异常。
使用析构函数:在析构函数中调用 Dispose 方法,以确保在垃圾回收时资源也能被释放。
使用 GC.SuppressFinalize:防止垃圾回收器再次调用析构函数。
4.6.3.Dispose示例代码
using System;
class ResourceHandler : IDisposable
{
// 非托管资源
private IntPtr unmanagedResource;
// 托管资源
private IDisposable managedResource;
// 资源是否已释放的标志
private bool disposed = false;
public ResourceHandler()
{
// 初始化资源
unmanagedResource = IntPtr.Zero;
managedResource = new SomeManagedResource();
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
{
// 释放托管资源
if (managedResource != null)
{
managedResource.Dispose();
managedResource = null;
}
}
// 释放非托管资源
if (unmanagedResource != IntPtr.Zero)
{
// 释放非托管资源的逻辑
unmanagedResource = IntPtr.Zero;
}
disposed = true;
}
}
~ResourceHandler()
{
Dispose(false);
}
}
class SomeManagedResource : IDisposable
{
public void Dispose()
{
// 释放托管资源
}
}
4.6.4.Dispose 方法详解表格
*特性* | *详细描述* |
---|---|
定义 | Dispose 方法是 IDisposable 接口中的唯一方法,用于释放托管资源和非托管资源。 |
接口 | IDisposable |
方法签名 | void Dispose(); |
调用方式 | 手动调用:instance.Dispose(); 或 using 语句自动调用。 |
主要用途 | 显式释放非托管资源(如文件句柄、数据库连接等)以及可选的托管资源。 |
实现步骤 | 1. 实现 IDisposable 接口。 2. 在 Dispose 方法中释放资源。 3. 使用 GC.SuppressFinalize 防止垃圾回收器再次调用析构函数。 |
调用时机 | 在对象不再需要使用时,或需要尽快释放资源时调用。 |
典型模式 | Dispose 模式(结合析构函数使用,确保资源最终释放)。 |
优点 | 1. 提高资源管理的灵活性和效率。 2. 防止资源泄漏。 3. 可以即时释放资源,而不是依赖垃圾回收。 |
注意事项 | 1. Dispose 方法可以多次调用,确保不抛出异常。 2. 在 Dispose 方法中确保托管资源和非托管资源都能被正确释放。 3. 在析构函数中调用 Dispose,以确保垃圾回收时也能释放资源。 |
4.6.5.总结
Dispose 方法提供了一种显式释放资源的机制,特别适用于管理非托管资源。通过实现 IDisposable 接口和定义 Dispose 方法,可以确保资源在不再需要时被及时释放,避免资源泄漏,提高应用程序的性能和稳定性。结合析构函数的使用,可以确保即使在垃圾回收时资源也能被正确释放。
4.7.GC、析构函数、IDisPose方法对比
4.7.1.表格对比
*特性* | *GC(垃圾回收器)* | *析构函数* | *IDisposable* *接口* |
---|---|---|---|
定义 | .NET运行时中的一个组件,用于自动管理内存和回收不再使用的托管对象。 | 在对象销毁时执行的特殊方法,用于清理资源。 | 用于显式释放资源的接口,通过实现 Dispose 方法来管理资源。 |
用途 | 管理托管内存,自动回收不再使用的对象,防止内存泄漏。 | 在对象销毁前执行最后的清理工作,通常用于释放非托管资源。 | 提供一种机制,允许对象持有者显式释放资源,特别是非托管资源。 |
管理资源类型 | 主要管理托管资源(由CLR管理的内存)。 | 主要用于释放非托管资源(如文件句柄、数据库连接等)。 | 既可以释放托管资源,也可以释放非托管资源。 |
调用时机 | 自动调用,不确定的时间点,由运行时环境决定。 | 由垃圾回收器在对象销毁时调用,不确定的时间点。 | 由开发者手动调用,一般在使用完资源后立即调用。 |
实现方式 | 内置于 .NET 运行时,开发者无需手动实现。 | 在类中定义一个析构函数,前面加 ~ 符号,系统在适当的时候调用。 | 类实现 IDisposable 接口,并实现 Dispose 方法。 |
示例代码 | GC.Collect(); | ~MyClass() { /* 资源清理代码 */ } | public void Dispose() { /* 资源清理代码 */ } |
适用场景 | 自动内存管理,适用于大多数托管对象的内存回收。 | 需要在对象销毁前执行一些清理操作,例如关闭文件、释放句柄等。 | 需要显式释放资源,例如文件、数据库连接、网络连接等。 |
性能影响 | 自动进行,通常效率高,但在GC运行时可能会导致短暂的性能下降(暂停)。 | 调用时间不确定,可能会延迟资源释放,影响性能。 | 由开发者控制,可以即时释放资源,提高性能。 |
使用注意事项 | 1. 避免频繁调用 GC.Collect(),以免影响性能。 | 1. 避免在析构函数中执行长时间操作。 2. 避免抛出异常。 | 1. 确保 Dispose 可以安全多次调用。 2. 使用 using 语句自动调用 Dispose 方法。 |
示例代码 | GC.Collect(); | ~MyClass() { /* 资源清理代码 */ } | public void Dispose() { /* 资源清理代码 */ } |
使用场景 | 适用于所有托管对象的内存回收。 | 适用于需要在对象销毁前执行清理操作的类。 | 适用于需要显式释放资源的类,特别是非托管资源。 |
4.7.2.示例代码
4.7.2.1.GC(垃圾回收器)
class Program
{
static void Main(string[] args)
{
// 强制进行垃圾回收
GC.Collect();
GC.WaitForPendingFinalizers();
}
}
4.7.2.2.析构函数
using System;
using System.IO;
class FileHandler
{
private FileStream fileStream;
public FileHandler(string filePath)
{
fileStream = new FileStream(filePath, FileMode.OpenOrCreate);
}
~FileHandler()
{
if (fileStream != null)
{
fileStream.Close();
Console.WriteLine("文件流已关闭");
}
}
}
class Program
{
static void Main(string[] args)
{
FileHandler handler = new FileHandler("example.txt");
}
}
4.7.2.3.IDisposable 接口
using System;
using System.IO;
class FileHandler : IDisposable
{
private FileStream fileStream;
private bool disposed = false;
public FileHandler(string filePath)
{
fileStream = new FileStream(filePath, FileMode.OpenOrCreate);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
{
if (fileStream != null)
{
fileStream.Close();
fileStream = null;
Console.WriteLine("文件流已关闭");
}
}
disposed = true;
}
}
~FileHandler()
{
Dispose(false);
}
}
class Program
{
static void Main(string[] args)
{
using (FileHandler handler = new FileHandler("example.txt"))
{
// 使用文件
}
// 离开using块时,会自动调用Dispose方法
}
}
4.8.C# 访问修饰符
4.8.1.什么是访问修饰符?
访问修饰符(Access Modifiers)是用于控制类及其成员(如字段、方法、属性等)访问权限的关键字。通过使用访问修饰符,可以定义哪些代码可以访问类或类成员,从而实现封装和信息隐藏。
4.8.2.C# 中有哪些访问修饰符
C# 提供了以下几种访问修饰符:
(1)public:公开访问,不受限制。
(2)private:私有访问,只有同一个类内部可以访问。
(3)protected:受保护访问,只有同一个类或派生类内部可以访问。
(4)internal:内部访问,只有同一个程序集内的代码可以访问。
(5)protected internal:受保护的内部访问,只有同一个程序集内的代码或派生类可以访问。
(6)private protected:私有的受保护访问,只有同一个类内部或同一个程序集内的派生类可以访问。
访问修饰符详解表格
*访问修饰符* | *访问范围* | *适用场景* |
---|---|---|
public | 无限制,任何地方都可以访问。 | 当需要类或成员对所有代码都可见时使用。 |
private | 仅限同一个类内部访问。 | 当需要隐藏类成员,防止外部代码直接访问时使用。 |
protected | 仅限同一个类或派生类内部访问。 | 当需要在类和其派生类之间共享成员时使用。 |
internal | 仅限同一个程序集内部访问。 | 当需要在同一个程序集内共享类或成员,但不希望被其他程序集访问时使用。 |
protected internal | 仅限同一个程序集内的代码或派生类访问。 | 当需要在同一个程序集内或跨程序集的派生类之间共享成员时使用。 |
private protected | 仅限同一个类内部或同一个程序集内的派生类访问。 | 当需要在同一个类和同一个程序集内的派生类之间共享成员,但不希望其他程序集访问时使用。 |
4.8.3.访问修饰符详解
4.8.3.1. public
public 访问修饰符允许类或成员对所有代码可见,无论是同一个程序集还是不同程序集。使用 public 修饰符,可以实现最开放的访问权限。
public class MyClass
{
public int MyField;
public void MyMethod()
{
// 公开的方法
}
}
4.8.3.2.private
private 访问修饰符限制类或成员的访问权限,只允许在同一个类内部访问。使用 private 修饰符,可以隐藏类成员,防止外部代码直接访问。
public class MyClass
{
private int MyField;
private void MyMethod()
{
// 私有的方法
}
}
4.8.3.3.protected
protected 访问修饰符允许类或成员在同一个类或派生类内部访问。使用 protected 修饰符,可以在类和其派生类之间共享成员。
public class BaseClass
{
protected int MyField;
protected void MyMethod()
{
// 受保护的方法
}
}
public class DerivedClass : BaseClass
{
public void AccessProtectedMembers()
{
MyField = 10; // 访问基类的受保护成员
MyMethod(); // 调用基类的受保护方法
}
}
4.8.3.4.internal
internal 访问修饰符允许类或成员在同一个程序集内部访问。使用 internal 修饰符,可以在同一个程序集内共享类或成员,但不希望被其他程序集访问。
internal class MyClass
{
internal int MyField;
internal void MyMethod()
{
// 内部的方法
}
}
4.8.3.5.protected internal
protected internal 访问修饰符允许类或成员在同一个程序集内的代码或派生类访问。使用 protected internal 修饰符,可以在同一个程序集内或跨程序集的派生类之间共享成员。
public class BaseClass
{
protected internal int MyField;
protected internal void MyMethod()
{
// 受保护的内部方法
}
}
public class DerivedClass : BaseClass
{
public void AccessProtectedInternalMembers()
{
MyField = 10; // 访问基类的受保护的内部成员
MyMethod(); // 调用基类的受保护的内部方法
}
}
4.8.3.6.private protected
private protected 访问修饰符允许类或成员在同一个类内部或同一个程序集内的派生类访问。使用 private protected 修饰符,可以在同一个类和同一个程序集内的派生类之间共享成员,但不希望其他程序集访问。
public class BaseClass
{
private protected int MyField;
private protected void MyMethod()
{
// 私有的受保护方法
}
}
public class DerivedClass : BaseClass
{
public void AccessPrivateProtectedMembers()
{
MyField = 10; // 访问基类的私有的受保护成员
MyMethod(); // 调用基类的私有的受保护方法
}
}
4.8.4.总结
通过合理使用访问修饰符,可以有效地控制类及其成员的访问权限,增强代码的封装性和安全性。根据需要选择合适的访问修饰符,既可以保护内部实现细节,又可以提供必要的接口供外部使用。
4.9.封装
4.9.1.什么是封装?
封装是将对象的内部数据和实现细节隐藏起来,只通过公共方法与外界交互。这样可以保护数据,防止外部代码直接访问和修改。
4.9.2.如何实现封装?
要实现数据封装需要用到两个元素:私有字段和公共方法;
(1)将类的字段声明为私有,只能在类内部访问。
(2)通过公共的 getter 和 setter 方法访问和修改这些字段。C# 提供了属性(Properties)功能,用于简化 getter 和 setter 的编写。
// 定义一个 Person 类来表示个人
class Person
{
// 私有字段,表示个人的姓名和年龄
private string name; // 表示个人的姓名
private int age; // 表示个人的年龄
// 公共属性 Name,用于获取和设置 name 字段
public string Name
{
get { return name; } // 获取 name 字段的值
set { name = value; } // 设置 name 字段的值
}
// 公共属性 Age,用于获取和设置 age 字段,可以用来校验数据
public int Age
{
get { return age; } // 获取 age 字段的值
set
{
if (value >= 0) // 确保年龄是非负数
{
age = value; // 设置 age 字段的值
}
}
}
}
// 测试程序类
class Program
{
static void Main(string[] args)
{
// 创建一个 Person 类的实例
Person person = new Person();
// 设置姓名和年龄
person.Name = "Alice";
person.Age = 30;
// 输出姓名和年龄
Console.WriteLine($"Name: {person.Name}, Age: {person.Age}");
}
}
4.9.3.封装的优点
*优点* | *解释* |
---|---|
数据保护 | 防止外部代码直接修改内部数据。 |
灵活性 | 通过控制访问方法,可以轻松修改数据逻辑。 |
4.9.4.访问修饰符
*修饰符* | *说明* |
---|---|
public | 任何代码都能访问。 |
private | 只能在类内部访问。 |
protected | 只能在类内部和派生类中访问。 |
4.9.5.总结
封装通过隐藏对象内部的实现细节,只允许通过公共方法和属性与外界交互,从而保护数据的完整性和安全性,提高代码的灵活性和可维护性。
4.10.抽象
4.10.1.什么是抽象?
抽象是指隐藏细节,突出核心功能,使程序员专注于对象的主要功能。具体来说,抽象是将复杂系统的某些特性和行为提取出来,简化系统的设计和使用。
我们还是以电视遥控器为例来说明,
(1)内部细节:电路板、红外线发射器等。
(2)核心功能:打开电视、调节音量、换频道等。
但你只需按下按钮,电视就会执行相应的操作,而不需要了解遥控器内部的工作原理。这就是抽象,隐藏复杂的内部细节,突出简单的使用功能。
4.10.2.实现抽象的两种方法
(1)抽象类:不能实例化,可以包含抽象方法(没有实现的方法)和具体方法(有实现的方法)。
(2)接口:定义一组方法和属性,没有实现,类可以实现多个接口。
4.10.3.抽象类
抽象类使用 abstract 关键字定义。抽象类中可以包含抽象方法和具体方法。
abstract class Animal
{
// 抽象方法(没有方法体)
public abstract void MakeSound();
// 具体方法
public void Sleep()
{
Console.WriteLine("Sleeping...");
}
}
class Dog : Animal
{
// 实现抽象方法
public override void MakeSound()
{
Console.WriteLine("Bark");
}
}
class Program
{
static void Main()
{
Dog dog = new Dog();
dog.MakeSound(); // 输出:Bark
dog.Sleep(); // 输出:Sleeping...
}
}
4.10.4.抽象类的特点
*特点* | *解释* |
---|---|
不能实例化 | 不能创建抽象类的对象。 |
可以包含具体方法 | 抽象类可以包含具体方法(有实现的方法)。 |
可以包含抽象方法 | 抽象类可以包含抽象方法(没有实现的方法)。 |
需要被继承 | 必须由子类继承并实现其抽象方法。 |
4.10.5.接口
接口使用 interface 关键字定义,接口中只包含方法和属性的声明,没有实现。
interface IAnimal
{
void MakeSound();
}
class Cat : IAnimal
{
// 实现接口方法
public void MakeSound()
{
Console.WriteLine("Meow");
}
}
class Program
{
static void Main()
{
IAnimal cat = new Cat();
cat.MakeSound(); // 输出:Meow
}
}
4.10.6.接口的特点
*特点* | *解释* |
---|---|
不能实例化 | 不能创建接口的对象。 |
没有实现 | 接口中的方法和属性没有实现。 |
需要被实现 | 必须由类实现接口中的所有方法和属性。 |
多重继承 | 一个类可以实现多个接口。 |
4.10.7.抽象类 vs 接口
*特点* | *抽象类* | *接口* |
---|---|---|
实现 | 可以包含具体实现 | 不能包含具体实现 |
多重继承 | 不能多重继承 | 可以实现多个接口 |
字段 | 可以包含字段 | 不能包含字段 |
访问修饰符 | 可以使用访问修饰符 | 默认是 public,不能使用访问修饰符 |
4.10.8.什么时候使用抽象类和接口?
使用抽象类:
- 当你需要为一组相关对象提供通用的基类,并包含一些共有的实现细节。
- 当你有一些方法需要在基类中实现,并且其他方法需要在派生类中实现。
使用接口:
- 当你需要为不相关的类提供一些通用行为。
- 当你需要实现多重继承时,因为C#不支持类的多重继承,但支持接口的多重实现。
4.10.9.示例对比
4.10.9.1.抽象类示例
abstract class Shape
{
public abstract double GetArea();
}
class Circle : Shape
{
private double radius;
public Circle(double radius)
{
this.radius = radius;
}
public override double GetArea()
{
return Math.PI * radius * radius;
}
}
class Program
{
static void Main()
{
Shape circle = new Circle(5);
Console.WriteLine($"Area of circle: {circle.GetArea()}");
}
}
4.10.9.2.接口示例
interface IDrawable
{
void Draw();
}
class Square : IDrawable
{
public void Draw()
{
Console.WriteLine("Drawing a square");
}
}
class Program
{
static void Main()
{
IDrawable square = new Square();
square.Draw();
}
}
4.10.10.总结
抽象通过隐藏对象的复杂性,只显示必要的功能,使程序员专注于对象的主要功能,忽略内部实现细节。C#通过抽象类和接口实现抽象,允许开发者创建灵活、易维护的代码。理解抽象类和接口的使用场景,是掌握C#面向对象编程的重要一步。
4.11.继承
继承是面向对象编程(OOP)中一个重要的概念,它允许一个类(子类)继承另一个类(父类)的属性和方法,从而实现代码的复用和扩展。通过继承,子类不仅可以使用父类的功能,还可以增加新的功能或修改现有的功能。
4.11.1.什么是继承?
继承是指一个类(子类)从另一个类(父类)获得属性和方法的机制。继承使得代码更易于维护和扩展。交通工具继承:
父类:交通工具
子类:汽车、飞机、船
子类继承了父类的基本特征(如可以移动、运送人和货物),并可以拥有自己的特征(如汽车有轮子,飞机有翅膀,船在水上行驶)。
4.11.2.C#中的继承
在C#中,使用冒号(:)来表示继承。子类从父类继承属性和方法,但也可以添加新的属性和方法,或者重写父类的方法。基本语法如下:
class Parent
{
// 父类的属性和方法
}
class Child : Parent
{
// 子类的属性和方法
}
4.11.3.继承示例
// 父类
class Animal
{
public void Eat()
{
Console.WriteLine("Eating...");
}
}
// 子类
class Dog : Animal
{
public void Bark()
{
Console.WriteLine("Barking...");
}
}
class Program
{
static void Main()
{
Dog dog = new Dog();
dog.Eat(); // 调用父类的方法
dog.Bark(); // 调用子类的方法
}
}
4.11.4.继承的类型
4.11.4.1.单继承
单继承是指一个子类只能继承一个父类。C#只支持单继承。
class Parent
{
// 父类的属性和方法
}
class Child : Parent
{
// 子类的属性和方法
}
4.11.4.2.多重继承
多重继承是指一个子类可以继承多个父类。C#不支持多重继承,但可以通过接口实现类似的功能。
interface IParent1
{
void Method1();
}
interface IParent2
{
void Method2();
}
class Child : IParent1, IParent2
{
public void Method1() { /* 实现 IParent1 的方法 */ }
public void Method2() { /* 实现 IParent2 的方法 */ }
}
4.11.5.方法重写
子类可以使用 override 关键字重写父类的方法。父类的方法必须使用 virtual 或 abstract 关键字标记为可重写。
// 定义一个抽象类
abstract class BaseClass
{
// 抽象方法,没有方法体,需要在派生类中实现
public abstract void AbstractMethod();
// 虚方法,有默认的实现,可以在派生类中重写
public virtual void VirtualMethod()
{
Console.WriteLine("BaseClass VirtualMethod");
}
// 具体方法,有实现
public void ConcreteMethod()
{
Console.WriteLine("BaseClass ConcreteMethod");
}
}
// 派生类实现抽象方法并重写虚方法
class DerivedClass : BaseClass
{
// 实现抽象方法
public override void AbstractMethod()
{
Console.WriteLine("DerivedClass AbstractMethod");
}
// 重写虚方法
public override void VirtualMethod()
{
Console.WriteLine("DerivedClass VirtualMethod");
}
}
class Program
{
static void Main()
{
BaseClass obj = new DerivedClass();
obj.AbstractMethod(); // 输出:DerivedClass AbstractMethod
obj.VirtualMethod(); // 输出:DerivedClass VirtualMethod
obj.ConcreteMethod(); // 输出:BaseClass ConcreteMethod
}
}
4.11.6.方法隐藏
方法隐藏(Method Hiding)允许在子类中定义一个与父类中方法同名的新方法。通过使用 new 关键字,子类的方法会隐藏父类的方法。与方法重写不同,方法隐藏不会改变父类方法的行为。
class Parent
{
public void Show()
{
Console.WriteLine("Parent Show");
}
}
class Child : Parent
{
public new void Show()
{
Console.WriteLine("Child Show");
}
}
class Program
{
static void Main()
{
Parent parent = new Parent();
parent.Show(); // 输出:Parent Show
Child child = new Child();
child.Show(); // 输出:Child Show
Parent parentAsChild = new Child();
parentAsChild.Show(); // 输出:Parent Show
}
}
4.11.7.方法隐藏 vs 方法重写
*特点* | *方法隐藏* | *方法重写* |
---|---|---|
是否改变父类方法的行为 | 不改变父类方法的行为 | 改变父类方法的行为 |
关键字 | new | override |
是否要求父类方法标记为虚方法 | 否 | 是 |
行为差异 | 通过父类引用调用方法时调用父类方法 | 通过父类引用调用方法时调用子类方法 |
4.11.8.访问修饰符继承
在继承中,访问修饰符决定了类成员在继承中的可见性和访问权限。
4.11.8.1.公有继承
公有继承意味着子类继承父类的公共成员和受保护成员,并保持它们的访问权限。
class Parent
{
public void PublicMethod()
{
Console.WriteLine("Parent Public Method");
}
protected void ProtectedMethod()
{
Console.WriteLine("Parent Protected Method");
}
}
class Child : Parent
{
public void AccessMethods()
{
PublicMethod(); // 可以访问
ProtectedMethod(); // 可以访问
}
}
4.11.8.2.私有继承
私有继承(C# 中没有直接支持,但可以通过组合实现类似功能)意味着子类将父类的公共成员和受保护成员继承为私有成员。
class Parent
{
public void PublicMethod()
{
Console.WriteLine("Parent Public Method");
}
protected void ProtectedMethod()
{
Console.WriteLine("Parent Protected Method");
}
}
class Child : Parent
{
public new void AccessMethods()
{
PublicMethod(); // 可以访问
ProtectedMethod(); // 可以访问,但不能通过实例化类来访问
}
}
4.11.9.总结
继承是C#中实现代码复用和扩展的强大机制。通过继承,子类不仅可以使用父类的功能,还可以增加新的功能或修改现有的功能。理解并正确使用继承,可以大大提高代码的复用性、扩展性和维护性。通过上面的示例代码和解释,可以更好地理解继承、方法重写、方法隐藏以及访问修饰符在继承中的作用和区别。
4.12.继承和组合
4.12.1.继承
继承允许一个类(子类)从另一个类(基类)继承属性和方法。子类可以扩展或修改基类的行为。继承体现了“是一个”(is-a)关系,比如狗是动物的一种。继承层次过深会导致代码复杂度增加,不易维护,过度使用继承可能会导致“脆弱的基类问题”。
*特点* | *说明* |
---|---|
代码复用 | 子类继承基类的属性和方法,减少代码重复 |
扩展性 | 子类可以添加新的方法和属性,也可以重写基类的方法 |
多态性 | 通过基类引用可以指向子类对象,执行子类的实现 |
// 基类
public class Animal
{
public void Eat()
{
Console.WriteLine("Eating...");
}
}
// 子类
public class Dog : Animal
{
public void Bark()
{
Console.WriteLine("Barking...");
}
}
public class Program
{
public static void Main()
{
Dog dog = new Dog();
dog.Eat(); // 基类的方法
dog.Bark(); // 子类的方法
}
}
4.12.2.组合(Composition)
组合是将一个类的实例作为成员变量包含在另一个类中。组合体现了“有一个”(has-a)关系,比如汽车有一个引擎。组合关系在设计时需要考虑好组件之间的依赖关系,避免过度依赖,过多的组合可能会导致类的接口过于复杂。
*特点* | *说明* |
---|---|
灵活性 | 类的行为可以通过组合不同的组件来实现,且组件可以独立变化 |
代码复用 | 通过组合可以实现代码复用,而不需要使用继承 |
降低耦合 | 组件之间的依赖性较低,类的变化不会影响到其他类 |
// 组成部分类
public class Engine
{
public void Start()
{
Console.WriteLine("Engine starting...");
}
}
// 使用组合的类
public class Car
{
private Engine engine = new Engine();
public void Start()
{
engine.Start();
Console.WriteLine("Car starting...");
}
}
public class Program
{
public static void Main()
{
Car car = new Car();
car.Start(); // 调用组合对象的方法
}
}
4.12.3.继承与组合的选择
继承适用于需要在子类中扩展或修改基类行为的情况,强调“是一个”的关系。
组合适用于通过不同组件组合实现类的行为,强调“有一个”的关系。
在实际开发中,更推荐优先使用组合,只有在明确表示子类和基类之间是“is-a”关系时才使用继承。这种方式可以让代码更加灵活、易于维护和扩展。
4.13.泛化与特化
泛化(Generalization)和特化(Specialization)是面向对象编程中的两个重要概念,它们分别对应了类之间的抽象和具体关系。在C#中,这些概念通过类继承和接口实现得以体现。
4.13.1.泛化(Generalization)
泛化是指将具体的类抽象成更通用的基类或接口,使得多个具体类可以共享相同的属性和方法。这种方式提高了代码的复用性和可维护性。假设我们有两种类型的动物:猫和狗。它们都可以吃东西和睡觉。我们可以将这些共同的行为抽象到一个基类 Animal 中。
// 基类
public class Animal
{
public void Eat()
{
Console.WriteLine("Eating...");
}
public void Sleep()
{
Console.WriteLine("Sleeping...");
}
}
// 具体类
public class Dog : Animal
{
public void Bark()
{
Console.WriteLine("Barking...");
}
}
public class Cat : Animal
{
public void Meow()
{
Console.WriteLine("Meowing...");
}
}
public class Program
{
public static void Main()
{
Dog dog = new Dog();
dog.Eat(); // 来自Animal类
dog.Sleep(); // 来自Animal类
dog.Bark(); // 来自Dog类
Cat cat = new Cat();
cat.Eat(); // 来自Animal类
cat.Sleep(); // 来自Animal类
cat.Meow(); // 来自Cat类
}
}
在这个例子中,Animal 类是一个泛化的基类,它包含了所有动物共有的行为。Dog 和 Cat 类是具体的类,它们分别实现了各自特有的行为。
4.13.2.特化(Specialization)
特化是指从一个通用的基类派生出更具体的类。这些具体类在继承了基类的属性和方法的同时,还可以添加自己的属性和方法。特化的过程通常意味着类的功能变得更加具体和详细。继续上面的例子,假设我们有一个更加具体的需求,狗可以训练,而猫可以捉老鼠。
// 具体类
public class TrainedDog : Dog
{
public void PerformTrick()
{
Console.WriteLine("Performing a trick...");
}
}
public class HuntingCat : Cat
{
public void Hunt()
{
Console.WriteLine("Hunting a mouse...");
}
}
public class Program
{
public static void Main()
{
TrainedDog trainedDog = new TrainedDog();
trainedDog.Eat(); // 来自Animal类
trainedDog.Sleep(); // 来自Animal类
trainedDog.Bark(); // 来自Dog类
trainedDog.PerformTrick(); // 来自TrainedDog类
HuntingCat huntingCat = new HuntingCat();
huntingCat.Eat(); // 来自Animal类
huntingCat.Sleep(); // 来自Animal类
huntingCat.Meow(); // 来自Cat类
huntingCat.Hunt(); // 来自HuntingCat类
}
}
在这个例子中,TrainedDog 和 HuntingCat 是从 Dog 和 Cat 特化出来的具体类,它们各自增加了新的行为。
*概念* | *定义* | *示例* |
---|---|---|
泛化 | 将具体类抽象为通用的基类或接口 | Animal 类 |
特化 | 从通用的基类派生出更具体的类 | TrainedDog 和 HuntingCat |
4.13.3.泛化和特化的关系
泛化:从具体到抽象(多个具体类共享一个基类)。
特化:从抽象到具体(基类派生出更具体的子类)。
4.14.接口
4.14.1.什么是接口?
接口类似于一个契约或协议,规定了实现它的类必须提供的方法和属性,但不包含具体的实现细节。接口只定义方法、属性、事件或索引器的签名。
4.14.2.定义接口
在C#中,接口使用 interface 关键字定义。接口的命名通常以大写字母 I 开头,以表示这是一个接口。
public interface IAnimal
{
void Eat();
void Sleep();
}
4.14.3.实现接口
类使用 : interface_name 语法来实现接口,并提供接口中定义的方法的具体实现。
public class Dog : IAnimal
{
public void Eat()
{
Console.WriteLine("Dog is eating.");
}
public void Sleep()
{
Console.WriteLine("Dog is sleeping.");
}
}
public class Cat : IAnimal
{
public void Eat()
{
Console.WriteLine("Cat is eating.");
}
public void Sleep()
{
Console.WriteLine("Cat is sleeping.");
}
}
4.14.4.接口的特点
*特点* | *说明* |
---|---|
定义 | 使用 interface 关键字定义,不包含实现细节 |
实现 | 类使用 : interface_name 实现接口 |
多重继承 | 一个类可以实现多个接口,从而实现多重继承效果 |
解耦 | 接口将实现细节与使用者分离,提高代码的灵活性和可维护性 |
多态性 | 接口允许不同的类以相同的方式被使用,实现方法的多态性 |
设计灵活性 | 通过接口定义API,可以灵活地更换实现,而不影响API使用者 |
依赖注入 | 接口是实现依赖注入的基础,通过接口注入不同的实现类,可以灵活地更换功能模块 |
4.14.5.接口的使用场景
接口在以下场景中非常有用:
(1)设计灵活的API:通过接口定义API,可以灵活地更换实现,而不影响API使用者。
(2)模拟多重继承:C#中不支持类的多重继承,但一个类可以实现多个接口,从而实现类似多重继承的效果。
(3)依赖注入:接口是实现依赖注入(Dependency Injection)的基础,通过接口注入不同的实现类,可以灵活地更换功能模块。
(4)以下是一个完整的示例代码,展示如何定义和实现接口,以及如何使用接口实现多态性和依赖注入。
using System;
public interface IAnimal
{
void Eat();
void Sleep();
}
public class Dog : IAnimal
{
public void Eat()
{
Console.WriteLine("Dog is eating.");
}
public void Sleep()
{
Console.WriteLine("Dog is sleeping.");
}
}
public class Cat : IAnimal
{
public void Eat()
{
Console.WriteLine("Cat is eating.");
}
public void Sleep()
{
Console.WriteLine("Cat is sleeping.");
}
}
public class AnimalShelter
{
private readonly IAnimal _animal;
public AnimalShelter(IAnimal animal)
{
_animal = animal;
}
public void TakeCare()
{
_animal.Eat();
_animal.Sleep();
}
}
public class Program
{
public static void Main()
{
IAnimal dog = new Dog();
IAnimal cat = new Cat();
AnimalShelter shelterWithDog = new AnimalShelter(dog);
AnimalShelter shelterWithCat = new AnimalShelter(cat);
Console.WriteLine("Taking care of the dog:");
shelterWithDog.TakeCare();
Console.WriteLine("\nTaking care of the cat:");
shelterWithCat.TakeCare();
}
}
4.14.6.总结
接口是面向对象编程中实现多态性、解耦和设计灵活API的重要工具。通过接口,我们可以定义一组方法和属性的契约,实现类必须遵循这个契约,从而实现代码的模块化和灵活性。
*特点* | *说明* |
---|---|
定义 | 使用 interface 关键字定义,不包含实现细节 |
实现 | 类使用 : interface_name 实现接口 |
多重继承 | 一个类可以实现多个接口,从而实现多重继承效果 |
解耦 | 接口将实现细节与使用者分离,提高代码的灵活性和可维护性 |
多态性 | 接口允许不同的类以相同的方式被使用,实现方法的多态性 |
设计灵活性 | 通过接口定义API,可以灵活地更换实现,而不影响API使用者 |
依赖注入 | 接口是实现依赖注入的基础,通过接口注入不同的实现类,可以灵活地更换功能模块 |
4.15.多态
多态(Polymorphism)是面向对象编程(OOP)中的一个核心概念,它允许对象以多种形式存在。简单来说,多态性就是不同的对象可以通过相同的接口调用不同的实现方法。
4.15.1.什么是多态?
多态性指的是同一个方法在不同对象上有不同的表现。
举个例子:我们有三种手机,分别是华为、苹果、三星,每种手机都有“播放音乐“这个接口,但用不同的手机播放音乐,音效是不同的的,这就是多态,不同手机都有播放音乐的功能,但音效却千差万别。
4.15.2.多态的实现方式
在C#中,多态主要通过继承和接口来实现,具体表现为方法的重写和接口的实现。
4.15.2.1.方法重写(Override)
当一个子类继承自父类并重写父类的方法时,便实现了多态。重写使用 override 关键字。
public class Animal
{
public virtual void MakeSound()
{
Console.WriteLine("Animal sound");
}
}
public class Dog : Animal
{
public override void MakeSound()
{
Console.WriteLine("Bark");
}
}
public class Cat : Animal
{
public override void MakeSound()
{
Console.WriteLine("Meow");
}
}
public class Program
{
public static void Main()
{
Animal myDog = new Dog();//父类的引用指向子类的实例
Animal myDog = new Cat();
myDog.MakeSound(); // 输出:Bark
myCat.MakeSound(); // 输出:Meow
}
}
在这个例子中,Dog 和 Cat 类分别重写了 Animal 类的 MakeSound 方法。实例化了两个对象myDog、myDog,两个对象调用同一个方法,输出却不一样,这就是多态。
4.15.2.2.接口实现(Interface Implementation)
通过接口实现多态性,不同的类可以实现相同的接口,并提供不同的实现。
public interface IAnimal
{
void MakeSound();
}
public class Dog : IAnimal
{
public void MakeSound()
{
Console.WriteLine("Bark");
}
}
public class Cat : IAnimal
{
public void MakeSound()
{
Console.WriteLine("Meow");
}
}
public class Program
{
public static void Main()
{
IAnimal myDog = new Dog(); //接口的引用指向实现的实例
IAnimal myCat = new Cat();
myDog.MakeSound(); // 输出:Bark
myCat.MakeSound(); // 输出:Meow
}
}
在这个例子中,Dog 和 Cat 类实现了 IAnimal 接口,并提供了自己的 MakeSound 方法。
4.15.3.多态的好处
*优点* | *说明* |
---|---|
代码重用 | 通过继承和接口,子类可以重用父类的代码 |
灵活性 | 可以在运行时决定调用哪个方法,提高了代码的灵活性 |
可维护性 | 当需要修改行为时,只需修改特定类或接口的实现,其他代码不受影响 |
4.15.4.多态的使用场景
*使用场景* | *说明* |
---|---|
设计灵活的API | 通过接口定义API,可以灵活地更换实现 |
处理不同类型的对象 | 使用基类或接口类型的变量来处理不同的具体对象 |
简化代码 | 通过多态性,可以减少大量的条件语句和类型检查 |
*特点* | *说明* |
---|---|
方法重写 | 子类通过 override 关键字重写父类的方法,实现多态 |
接口实现 | 不同的类实现相同的接口,并提供不同的实现 |
代码重用 | 通过继承和接口,子类可以重用父类的代码 |
灵活性 | 可以在运行时决定调用哪个方法,提高了代码的灵活性 |
可维护性 | 当需要修改行为时,只需修改特定类或接口的实现,其他代码不受影响 |
4.16.方法重载
方法重载(Method Overloading)是面向对象编程(OOP)中的一个重要概念。它允许在同一个类中定义多个具有相同名称但参数不同的方法。通过方法重载,可以提高代码的灵活性和可读性。
4.16.1.什么是方法重载?
方法重载是指在一个类中可以定义多个同名的方法,但这些方法的参数列表(参数的类型、数量或顺序)必须不同。方法重载不依赖于方法的返回类型。
4.16.2.方法重载的实现
在C#中,方法重载通过定义多个同名但参数列表不同的方法来实现。以下是一个简单的示例:
public class Calculator
{
//第一个 `Add` 方法接受两个 `int` 类型的参数。
public int Add(int a, int b)
{
return a + b;
}
//第二个 `Add` 方法接受两个 `double` 类型的参数。
public double Add(double a, double b)
{
return a + b;
}
//第三个 `Add` 方法接受三个 `int` 类型的参数。
public int Add(int a, int b, int c)
{
return a + b + c;
}
}
4.16.3.方法重载的规则
参数数量不同:可以通过改变参数的数量来实现方法重载。
public void Print(string message) { }
public void Print(string message, int times) { }
参数类型不同:可以通过改变参数的类型来实现方法重载。
public void Print(string message) { }
public void Print(int number) { }
参数顺序不同:可以通过改变参数的顺序来实现方法重载。
public void Print(string message, int number) { }
public void Print(int number, string message) { }
4.16.4.方法重载的优点
*优点* | *说明* |
---|---|
代码灵活性 | 通过方法重载,可以使用同一个方法名称处理不同类型的参数 |
可读性 | 代码更易读,通过同一方法名称可以了解它们实现的相似功能 |
维护性 | 代码更易维护,通过方法重载可以减少方法名称的多样性 |
4.16.5.方法重载的缺点
*缺点* | *说明* |
---|---|
代码复杂性 | 过多的重载方法会增加代码的复杂性,可能导致难以维护和理解 |
调用歧义 | 如果参数类型不明确,可能会导致调用方法时的歧义 |
可读性下降 | 虽然重载可以提高灵活性,但也可能让代码变得不够直观 |
性能开销 | 编译器需要在编译时进行方法的选择,可能会带来性能上的开销 |
4.16.6.方法重载的使用场景
(1)不同类型的输入:需要处理不同类型的数据。
public void Save(int data) { }
public void Save(string data) { }
(2)不同数量的输入:需要处理不同数量的参数。
public void Log(string message) { }
public void Log(string message, Exception ex) { }
(3)相似功能的实现:需要实现类似功能但处理不同数据。
public int Calculate(int a, int b) { return a + b; }
public double Calculate(double a, double b) { return a + b; }
4.16.7.方法重载的注意事项
(1)返回类型:方法重载仅基于参数列表的不同,不考虑返回类型。
(2)可选参数:使用可选参数时,要注意与方法重载的冲突。
4.17.操作符重载
我们知道,基本的数据类型可以有很多操作符运算,比如1+4;2.5*19;45%9 等等,那么类可以进行操作符运算吗?
操作符重载是C# 中的一种功能,允许你自定义或修改运算符(如 +、-、* 等)的行为,让你的类实例可以像基本数据类型一样进行操作。
4.17.1.操作符重载的基本语法
在C# 中,操作符重载通过使用 operator 关键字进行定义。操作符重载必须作为类(结构)的公共静态方法进行定义,且至少有一个参数是该类或结构的类型。
4.17.2.基本语法
public static 返回类型 operator 操作符(参数列表)
{
// 操作逻辑
}
4.17.3.操作符重载清单
*操作符* | *描述* | *示例* | *重载方法签名* |
---|---|---|---|
+ | 加法 | a + b | public static 类型 operator +(类型 a, 类型 b) |
- | 减法 | a - b | public static 类型 operator -(类型 a, 类型 b) |
* | 乘法 | a * b | public static 类型 operator *(类型 a, 类型 b) |
/ | 除法 | a / b | public static 类型 operator /(类型 a, 类型 b) |
% | 取模 | a % b | public static 类型 operator %(类型 a, 类型 b) |
== | 相等 | a == b | public static bool operator ==(类型 a, 类型 b) |
!= | 不等 | a != b | public static bool operator !=(类型 a, 类型 b) |
> | 大于 | a > b | public static bool operator >(类型 a, 类型 b) |
< | 小于 | a < b | public static bool operator <(类型 a, 类型 b) |
>= | 大于等于 | a >= b | public static bool operator >=(类型 a, 类型 b) |
<= | 小于等于 | a <= b | public static bool operator <=(类型 a, 类型 b) |
4.17.4.示例代码
以 Point 类的加法操作符重载为例:
(1)定义一个 Point 类,其中包含 X 和 Y 两个属性。
(2)实现构造函数用于初始化 Point 对象。
(3)重载 + 操作符,定义如何将两个 Point 对象相加。
(4)重写 ToString 方法,便于输出 Point 对象的字符串表示
。
假设我们有一个表示二维点的类 Point,我们希望重载加法操作符 + 来实现两个点的相加:
public class Point
{
public int X { get; set; }
public int Y { get; set; }
public Point(int x, int y)
{
X = x;
Y = y;
}
// 重载加法操作符
public static Point operator +(Point p1, Point p2)
{
return new Point(p1.X + p2.X, p1.Y + p2.Y);
}
public override string ToString()
{
return $"({X}, {Y})";
}
}
public class Program
{
public static void Main()
{
Point p1 = new Point(2, 3);
Point p2 = new Point(4, 5);
Point p3 = p1 + p2;
Console.WriteLine(p3); // 输出: (6, 8)
}
}
4.18.部分类和部分方法
C# 中的部分类和部分方法是一种便捷的代码组织和管理方式,特别适用于大型项目。它们允许将类和方法拆分成多个文件或部分,从而更好地维护代码和协作开发。
4.18.1.部分类
部分类(Partial Classes)允许将一个类拆分成多个文件中的多个部分。编译时,编译器会将这些部分组合成一个完整的类。这对于大型项目,尤其是自动生成代码的工具非常有用。
4.18.2.部分类的基本语法
在每个文件中使用 partial 关键字定义类:
// File1.cs
public partial class MyClass
{
public void Method1()
{
Console.WriteLine("Method1");
}
}
// File2.cs
public partial class MyClass
{
public void Method2()
{
Console.WriteLine("Method2");
}
}
编译时,MyClass 类会包含 Method1 和 Method2 方法:
public class Program
{
public static void Main()
{
MyClass myClass = new MyClass();
myClass.Method1(); // 输出: Method1
myClass.Method2(); // 输出: Method2
}
}
4.18.3.部分方法
部分方法(Partial Methods)允许在部分类中声明一个方法,而在另一个部分中实现它。如果方法未实现,不会导致编译错误,也不会影响性能。这通常用于自动生成代码的场景,工具生成方法声明,用户在手动编写的部分中实现这些方法。部分方法的基本语法如下:
在一个部分类中声明部分方法:
// File1.cs
public partial class MyClass
{
partial void PartialMethod();
}
在另一个部分类文件中实现部分方法:
// File2.cs
public partial class MyClass
{
partial void PartialMethod()
{
Console.WriteLine("Partial Method Invoked");
}
public void InvokePartialMethod()
{
PartialMethod();
}
}
调用部分方法:
public class Program
{
public static void Main()
{
MyClass myClass = new MyClass();
myClass.InvokePartialMethod(); // 输出: Partial Method Invoked
}
}
4.18.4.总结
C# 的部分类和部分方法使得代码组织更加灵活和易于管理。部分类允许将一个类分散在多个文件中,部分方法则允许声明和实现分开,极大地提升了代码维护和协作开发的效率。
4.19.密封类和密封方法
密封类和密封方法用于控制继承和重写,确保类和方法的行为不被改变,提供更高的设计控制和安全性。本文将详细介绍它们的概念、用法和实际应用。
4.19.1.密封类(Sealed Class)
密封类(Sealed Class)是指不能被继承的类。使用 sealed 关键字可以声明一个密封类。这样做的好处是可以防止其他类从这个类派生,从而保护类的设计和实现不被更改。
4.19.1.1.密封类的基本语法
public sealed class MyClass
{
public void Display()
{
Console.WriteLine("Hello from MyClass");
}
}
在上述示例中,MyClass 是一个密封类,不能被其他类继承:
public class AnotherClass : MyClass // 编译错误:'MyClass' 不能被继承
{
}
4.19.1.2.密封类的应用场景
(1)安全性:当你希望确保一个类的实现不能被更改时,可以将其声明为密封类。
(2)性能优化:密封类可以在某些情况下提供更好的性能,因为编译器可以对其进行特殊优化。
4.19.2.密封方法(Sealed Method)
密封方法(Sealed Method)用于阻止继承类重写基类中的虚方法。只有在基类方法被声明为 virtual 或 abstract 的情况下,子类才能使用 sealed 关键字来密封该方法。
4.19.2.1.密封方法的基本语法
public class BaseClass
{
public virtual void Display()
{
Console.WriteLine("Hello from BaseClass");
}
}
public class DerivedClass : BaseClass
{
public sealed override void Display()
{
Console.WriteLine("Hello from DerivedClass");
}
}
public class FurtherDerivedClass : DerivedClass
{
public override void Display() // 编译错误:无法重写密封方法
{
Console.WriteLine("Hello from FurtherDerivedClass");
}
}
在上述示例中,DerivedClass 中的 Display 方法被密封,因此 FurtherDerivedClass 不能重写该方法。
4.19.2.2.密封方法的应用场景
(1)控制继承:当你希望在某个类层次中禁止进一步重写某个方法时,可以使用密封方法。
(2)防止意外更改:密封方法可以防止在派生类中对基类重要行为的意外更改。
4.20.扩展方法
扩展方法是C#中的一种特性,允许你向现有类型添加新方法,而无需修改原有类型。它们让代码更简洁、更易读。简单来说,就是你可以给已有的类型“贴上”新功能,而不需要去动原来的代码。
4.20.1.什么是扩展方法?
扩展方法是定义在静态类中的静态方法,但可以像实例方法一样调用。第一个参数使用 this 关键字,表示要扩展的类型。扩展方法既可以是无参的,也可以是有参的;
注意:扩展方法的本质是静态方法,因此可以通过类来调用,同时实例也可以调用扩展方法:
public static class StringExtensions
{
public static bool IsNullOrEmpty(this string value)
{
return string.IsNullOrEmpty(value);
}
}
// 通过实例调用扩展方法
string myString = "Hello";
bool result1 = myString.IsNullOrEmpty();
// 通过类名调用扩展方法
bool result2 = StringExtensions.IsNullOrEmpty(myString);
4.20.2.基本语法
public static class 扩展方法类
{
public static 返回类型 扩展方法名(this 扩展类型 参数列表)
{
// 方法逻辑
}
}
4.20.3.扩展方法的常见示例
(1)统计字符串单词数量
public static class StringExtensions
{
public static int WordCount(this string str)
{
if (string.IsNullOrWhiteSpace(str))
return 0;
string[] words = str.Split(' ', StringSplitOptions.RemoveEmptyEntries);
return words.Length;
}
}
public class Program
{
public static void Main()
{
string text = "Hello World from C#";
int wordCount = text.WordCount();//我们可以像调用 `string` 类型的方法一样使用 `WordCount`:
Console.WriteLine($"Word Count: {wordCount}"); // 输出: Word Count: 4
}
}
(2)判断整数是否为偶数:
int number = 4;
bool isEven = number.IsEven();
Console.WriteLine($"{number} is even: {isEven}"); // 输出: 4 is even: True
public static class IntExtensions
{
public static bool IsEven(this int number)
{
return number % 2 == 0;
}
}
(3)判断日期是否为周末:
DateTime today = DateTime.Now;
bool isWeekend = today.IsWeekend();
Console.WriteLine($"{today.ToShortDateString()} is weekend: {isWeekend}");
public static class DateTimeExtensions
{
public static bool IsWeekend(this DateTime date)
{
return date.DayOfWeek == DayOfWeek.Saturday || date.DayOfWeek == DayOfWeek.Sunday;
}
}
(4)查找大于给定值的所有元素:
public class Program
{
public static void Main()
{
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
List<int> greaterThanFive = numbers.FindGreaterThan(5);
Console.WriteLine(string.Join(", ", greaterThanFive)); // 输出: 6, 7, 8, 9, 10
}
}
public static class ListExtensions
{
public static List<int> FindGreaterThan(this List<int> list, int value)
{
return list.Where(x => x > value).ToList();
}
}
4.21.类的引用和实例化
4.21.1.联系与区别
在C#中,引用和实例化是理解面向对象编程的重要概念。下面的表格将列出它们之间的联系和区别。
方面 | 引用 | 实例化 |
---|---|---|
定义 | 引用是一个变量,它存储的是对象在内存中的地址,而不是对象本身。 | 实例化是使用 new 关键字创建一个类的对象。 |
作用 | 通过引用,可以访问和操作对象。 | 实例化用于创建对象实例,分配内存并初始化对象。 |
内存 | 引用变量存储在栈内存中,指向堆内存中的实际对象。 | 实例化时,对象被分配在堆内存中。 |
语法 | Person person2 = person1; | Person person = new Person(); |
变化 | 更改一个引用的对象会影响所有指向该对象的引用。 | 实例化创建一个新的对象实例,彼此独立。 |
用途 | 引用用于共享对象,允许多个变量指向同一个对象。 | 实例化用于创建新的对象实例,通常用于需要独立状态的对象。 |
例子 | Person person1 = new Person(); Person person2 = person1; person1.Name = “Alice”; Console.WriteLine(person2.Name); // 输出: Alice | Person person = new Person(); person.Name = “Alice”; Console.WriteLine(person.Name); // 输出: Alice |
注意事项 | 不同的引用可以指向同一个对象,更改一个引用会影响其他引用。 | 每次实例化都会创建一个新的对象实例,彼此独立。 |
4.21.2.示例代码
4.21.2.1.引用示例
public class Program
{
public static void Main()
{
Person person1 = new Person();
person1.Name = "Alice";
Person person2 = person1; // person2 引用了 person1 的对象
person1.Name = "Bob";
Console.WriteLine(person2.Name); // 输出: Bob
}
}
public class Person
{
public string Name { get; set; }
}
4.21.2.2.实例化示例
public class Program
{
public static void Main()
{
Person person1 = new Person();
person1.Name = "Alice";
Person person2 = new Person(); // 创建新的 Person 实例
person2.Name = "Bob";
Console.WriteLine(person1.Name); // 输出: Alice
Console.WriteLine(person2.Name); // 输出: Bob
}
}
public class Person
{
public string Name { get; set; }
}
5.异常处理
5.1. C# 中的异常处理
异常处理是C#中的重要概念,用于在程序运行时捕获和处理错误。通过 try-catch 块来实现,确保程序能稳健运行。
5.1.1.什么是异常?
异常是程序运行时发生的错误事件,例如:
(1)除以零:1/0,0 不能做除数
(2)数组越界:定义arry[10]; 访问 arry[11],超出索引边界
(3)文件未找到:指定路径下未找到文件
5.1.2.异常的类型
(1)编译时异常
编译时异常是在代码编译时发现的错误,通常是由于语法错误或类型错误引起的。这些错误必须在代码编译之前修正,否则程序无法运行。
public class Program
{
public static void Main()
{
// 缺少分号,导致语法错误
Console.WriteLine("Hello, World!")
}
}
(2)运行时异常
运行时异常是在程序运行期间发生的错误。常见的运行时异常包括:
① DivideByZeroException:尝试除以零时抛出。
② IndexOutOfRangeException:数组索引超出范围时抛出。
③ NullReferenceException:尝试访问空对象时抛出。
④ FileNotFoundException:尝试访问不存在的文件时抛出。
5.1.3.异常处理的基本结构
在C#中,通过 try-catch 块来处理异常。try 块包含可能引发异常的代码,catch 块捕获并处理异常。
try
{
// 可能引发异常的代码
}
catch (ExceptionType ex)
{
// 处理异常的代码
}
5.1.4.示例代码
以下是一个处理用户输入异常的示例。在这个例子中,我们要求用户输入一个数字,并处理可能发生的异常,比如输入不是数字或输入为空的情况。
using System;
public class Program
{
public static void Main()
{
try
{
Console.WriteLine("请输入一个数字:");
string userInput = Console.ReadLine();
int number = int.Parse(userInput);
Console.WriteLine("你输入的数字是: " + number);
}
catch (FormatException ex)
{
Console.WriteLine("输入的不是有效的数字: " + ex.Message);
}
catch (ArgumentNullException ex)
{
Console.WriteLine("输入不能为空: " + ex.Message);
}
catch (Exception ex)
{
Console.WriteLine("发生了一个错误: " + ex.Message);
}
}
}
5.2. 多重 Catch 块
在C#中,try 块可以跟随多个 catch 块,每个 catch 块用于捕获和处理不同类型的异常。这种方式允许程序对不同类型的异常采取不同的处理措施,从而提高代码的健壮性和可维护性。
5.2.1.基本结构
try
{
// 可能引发异常的代码
}
catch (SpecificExceptionType1 ex)
{
// 处理 SpecificExceptionType1 的代码
}
catch (SpecificExceptionType2 ex)
{
// 处理 SpecificExceptionType2 的代码
}
catch (Exception ex)
{
// 处理所有其他类型的异常
}
5.2.2.示例代码
以下是一个处理用户输入和计算的示例,展示了如何使用多个 catch 块来捕获和处理不同类型的异常。
using System;
public class Program
{
public static void Main()
{
try
{
Console.WriteLine("请输入第一个数字:");
string input1 = Console.ReadLine();
int number1 = int.Parse(input1);
Console.WriteLine("请输入第二个数字:");
string input2 = Console.ReadLine();
int number2 = int.Parse(input2);
int result = number1 / number2;
Console.WriteLine("结果是: " + result);
}
catch (FormatException ex)
{
Console.WriteLine("输入的不是有效的数字: " + ex.Message);
}
catch (DivideByZeroException ex)
{
Console.WriteLine("不能除以零: " + ex.Message);
}
catch (Exception ex)
{
Console.WriteLine("发生了一个错误: " + ex.Message);
}
}
}
5.2.3.解释
FormatException: 当用户输入的字符串不能转换为整数时,会抛出这个异常。例如,用户输入了字母或其他非数字字符。此时会输出 “输入的不是有效的数字” 的错误信息。
DivideByZeroException: 当用户尝试除以零时,会抛出这个异常。例如,用户输入的第二个数字为零。此时会输出 “不能除以零” 的错误信息。
Exception: 捕获所有其他未预料到的异常,并输出通用的错误信息。这样可以确保即使发生了未知错误,程序也不会崩溃,并能提供一些错误信息给用户。
5.2.4.总结
使用多个 catch 块处理不同类型的异常是C#中异常处理的常见方式。它能帮助你更有针对性地处理各种异常情况,提高程序的可靠性和可维护性。通过明确的异常处理逻辑,你可以确保程序在遇到问题时能够优雅地处理,并向用户提供有意义的错误信息。
5.3. Finally 块
finally 块中的代码无论是否发生异常都会执行,通常用于释放资源,如关闭文件或数据库连接。使用 finally 块可以确保资源得到正确释放,避免资源泄漏。
5.3.1.基本结构
try
{
// 可能引发异常的代码
}
catch (ExceptionType ex)
{
// 处理异常的代码
}
finally
{
// 始终执行的代码
}
5.3.2.示例代码
以下是一个简单的示例,展示了如何使用 finally 块来确保文件被正确关闭:
using System;
using System.IO;
public class Program
{
public static void Main()
{
StreamReader reader = null;
try
{
reader = new StreamReader("example.txt");
string content = reader.ReadToEnd();
Console.WriteLine("文件内容: " + content);
}
catch (FileNotFoundException ex)
{
Console.WriteLine("文件未找到: " + ex.Message);
}
catch (UnauthorizedAccessException ex)
{
Console.WriteLine("无访问权限: " + ex.Message);
}
catch (Exception ex)
{
Console.WriteLine("读取文件时发生错误: " + ex.Message);
}
finally
{
// 确保资源得到释放
if (reader != null)
{
reader.Close();
Console.WriteLine("文件已关闭");
}
}
}
}
5.3.3.解释
try 块: 包含可能引发异常的代码。在这里,我们尝试打开并读取文件 example.txt。
多个 catch 块: 捕获不同类型的异常并处理。包括 FileNotFoundException(文件未找到)、UnauthorizedAccessException(无访问权限)和一般的 Exception(所有其他异常)。
finally 块: 无论是否发生异常,finally 块中的代码都会执行。在这里,我们确保文件在读取后被关闭。如果文件被成功打开并读取,无论读取过程中是否发生异常,reader.Close() 都会被执行,确保文件资源被正确释放。
5.3.4.总结
使用 finally 块可以确保资源在使用后得到正确释放,避免资源泄漏问题。无论在 try 块中是否发生异常,finally 块中的代码总会执行。这使得 finally 块非常适合用于清理工作,如关闭文件、释放数据库连接等操作,确保程序的健壮性和稳定性。
5.4. 创建自定义异常
5.4.1.什么是自定义异常?
自定义异常是开发者定义的异常类,用于处理特定业务逻辑中的错误。它继承自 System.Exception 类,允许你提供更多的上下文和特定的错误信息,从而更好地处理异常。
5.4.2.为什么需要自定义异常?
(1)明确异常类型:使用自定义异常可以清楚地表示发生了哪种特定错误。
(2)提高代码可读性:使得异常处理逻辑更易读、易维护。
(3)更好的调试信息:提供更详细和具体的错误信息,帮助快速定位问题。
5.4.3.如何创建自定义异常
自定义异常类需要继承自 Exception 类,并且通常至少包含一个接受错误消息的构造函数。在业务逻辑中,可以通过 throw 关键字抛出自定义异常,并在适当的地方捕获并处理这些异常。
public class InvalidAgeException : Exception
{
public InvalidAgeException(string message) : base(message) { }
}
5.4.4.示例:验证年龄
假设我们需要验证一个人的年龄,如果年龄不在合理范围内,则抛出自定义异常。
public class Person
{
public int Age { get; set; }
public void SetAge(int age)
{
if (age < 0 || age > 150)
{
throw new InvalidAgeException("Age must be between 0 and 150.");//实例化InvalidAgeException这个类
}
Age = age;
}
}
public class Program
{
public static void Main()
{
try
{
Person person = new Person();
person.SetAge(200);
}
catch (InvalidAgeException ex)
{
Console.WriteLine($"Error: {ex.Message}");
}
}
}
在上述示例中,当设置无效年龄时,程序抛出 InvalidAgeException 异常,并在 catch 块中捕获并处理该异常。
5.5. 内部异常
5.5.1.什么是内部异常?
内部异常(Inner Exceptions)是指一个异常包含了引发它的另一个异常。它提供了详细的错误信息,有助于调试和错误追踪。
5.5.2.为什么使用内部异常?
(1)错误链:展示多个异常之间的因果关系。
(2)调试和日志记录:提供详细的错误信息。
(3)信息传递:在重新抛出异常时,保留原始错误信息。
5.5.3.创建内部异常
自定义异常类可以包含一个内部异常参数:
/// <summary>
/// 自定义异常类,包含内部异常
/// </summary>
public class MyCustomException : Exception
{
public MyCustomException(string message, Exception innerException)
: base(message, innerException) { }
}
5.5.4.使用内部异常
当捕获到一个异常,并希望抛出一个新的异常时,可以包含原始异常作为内部异常:
/// <summary>
/// 示例类
/// </summary>
public class Example
{
public void Method()
{
try
{
int result = int.Parse("abc"); // 这行代码会抛出 FormatException
}
catch (FormatException ex)
{
throw new MyCustomException("Parsing error occurred", ex); // 捕获原始异常并抛出自定义异常
}
}
}
/// <summary>
/// 主程序类
/// </summary>
public class Program
{
public static void Main()
{
try
{
Example example = new Example();
example.Method(); // 调用方法,捕获异常
}
catch (MyCustomException ex)
{
Console.WriteLine($"Exception: {ex.Message}"); // 输出自定义异常的信息
Console.WriteLine($"Inner Exception: {ex.InnerException.Message}"); // 输出内部异常的信息
}
}
}
5.6. 异常处理滥用
我先问大家一个问题:程序是在部署时报错好还是在调试时报错好?闭着眼睛说:当然是在调试时报错好,因为问题可以提前发现,等到部署时才报错,客户非得把你骂死。如果我是项目负责人,在做代码评审的时候,我看到这种情况不一定会骂人的,但如果我是开发人员那我就往死里用,出问题了客户又不是直接骂我,对不对?哈哈……而造成这些令人烦恼的问题、万恶的根源很可能就是try catch。
Try…Catch,直译过来就是,尝试去运行代码,如果不能运行,则捕获错误,用一句咱们能听懂的话:能干咱就干,不能干那就拉倒吧。
5.6.1.什么是异常处理滥用?
异常处理滥用是指在代码中过度或不正确地使用异常处理机制。尽管异常处理是提高代码健壮性的重要工具,但滥用它可能导致代码性能下降、可读性降低和逻辑混乱。
5.6.2.常见的异常处理滥用情况
(1)控制流使用异常:将异常用作控制流的一部分,而不是正常的逻辑分支。
try
{
// 不推荐:用异常来跳出循环
while (true)
{
// 可能的逻辑
throw new Exception();
}
}
catch (Exception ex)
{
// 异常处理
}
(2)捕获所有异常但不处理:捕获所有异常却没有适当的处理逻辑,只是简单地忽略或记录。
try
{
// 可能引发异常的代码
}
catch (Exception ex)
{
// 不推荐:仅记录或忽略异常
Console.WriteLine(ex.Message);
}
(3)过度捕获:捕获了不必要的异常,或者捕获异常范围过于广泛。
try
{
// 仅一个可能引发异常的小操作
int result = int.Parse("123");
}
catch (FormatException ex)
{
// 过度捕获:在没有必要的情况下捕获异常
Console.WriteLine(ex.Message);
}
5.6.3.避免异常处理滥用的最佳实践
(1)使用条件检查代替异常:在可能发生错误的地方,使用条件检查来避免异常。
int number;
if (int.TryParse("123", out number))
{
// 成功解析
}
else
{
// 解析失败处理逻辑
}
(2)明确异常处理目标:只捕获和处理那些你能做出有效响应的异常,不要滥用通用异常处理。
try
{
// 可能引发特定异常的代码
}
catch (FormatException ex)
{
// 处理特定异常
Console.WriteLine("Invalid format.");
}
(3)保持异常处理简单:只在必要时使用异常处理,避免过度复杂的异常处理逻辑。
(4)记录并传播异常:在适当的时候记录异常并重新抛出,以便在更高层级处理。
try
{
// 可能引发异常的代码
}
catch (Exception ex)
{
// 记录异常
Console.WriteLine(ex.Message);
// 重新抛出异常
throw;
}
6.委托、事件、Lambda表达式
6.1. 事件、委托和 Lambda表达式概述
6.1.1.事件、委托和 Lambda 表达式关系
(1)事件通过委托调用方法或者Lambda表达式。
(2)Lambda 表达式是简洁的方法,也叫匿名方法,和普通方法没有什么区别,只不过普通方法比Lambda表达式多了一个名字。
6.1.2.委托(Delegates)
委托是一个定义了方法签名的引用类型,类似于C++的函数指针,可用于存储和调用方法。方法签名就是方法的名字和它的参数(包括类型和个数),这两部分组合在一起可以用来区分不同的方法。比如,Add(int a, int b)
和 Add(double a, double b)
就是两个不同的方法签名,即使它们的名字相同。
// 定义一个委托
public delegate void MyDelegate(string message);
// 定义一个方法,这个方法的签名与委托匹配
public void ShowMessage(string message)
{
Console.WriteLine(message);
}
// 使用委托
MyDelegate del = ShowMessage;
del("Hello, Delegates!");
6.1.3.事件(Events)
事件是委托的封装,用于通知订阅者某个操作已经发生。事件通常用于实现观察者模式。
public class Publisher
{
// 定义一个事件
public event MyDelegate OnChange;
//触发事件的方法
public void RaiseEvent(string message)
{
if (OnChange != null)
{
OnChange(message);
}
}
}
//方法订阅事件
public class Subscriber
{
public void Subscribe(Publisher pub)
{
pub.OnChange += ShowMessage;
}
public void ShowMessage(string message)
{
Console.WriteLine(message);
}
}
// 使用事件
Publisher publisher = new Publisher();
Subscriber subscriber = new Subscriber();
subscriber.Subscribe(publisher);
publisher.RaiseEvent("Event raised!");
6.1.4.Lambda 表达式
Lambda 表达式是一种简洁的方法,也叫匿名函数。它可以用来替代委托和匿名方法,使代码更简洁。例如,假设你有一个委托 Func<int, int>,它接收一个整数并返回一个整数,你可以使用 Lambda 表达式来实现它:
Func<int, int> square = x => x * x;
int result = square(5);
Console.WriteLine(result); // 输出 25
6.2. 委托
6.2.1.什么是委托?
委托是C#中的一种引用类型,它用来存储和调用方法,功能类似于C++中的函数指针,但在类型安全性和灵活性上更强大。
6.2.1.1.C++函数指针的基本语法
#include <iostream>
using namespace std;
// 一个简单的函数,接收一个整数参数并打印它
void PrintNumber(int number)
{
cout << "The number is: " << number << endl;
}
int main()
{
// 定义一个函数指针,指向接收一个整数参数并返回 void 的函数
void (*funcPtr)(int);
// 将 PrintNumber 函数的地址赋值给函数指针
funcPtr = PrintNumber;
// 使用函数指针调用 PrintNumber 函数
funcPtr(5); // 输出: The number is: 5
return 0;
}
6.2.1.2.委托的基本语法
// 定义一个委托类型
public delegate void MyDelegate(string message);
// 定义一个与委托签名匹配的方法
public void ShowMessage(string message)
{
Console.WriteLine(message);
}
// 使用委托
MyDelegate del = ShowMessage;
del("Hello, Delegates!"); // 输出: Hello, Delegates!
6.2.2.为什么使用委托?
优点 | 解释 | 示例 |
---|---|---|
回调机制 | 将委托做为参数传递,并调用该委托 | 异步操作完成后通知主程序。 |
事件处理 | 常用于处理事件响应。 | 处理按钮点击事件。 |
灵活性 | 使代码更灵活,可动态绑定和解绑方法。 | 可以随时更改调用的方法。 |
可扩展性 | 允许动态添加或删除方法,增强程序功能。 | 运行时修改方法逻辑。 |
解耦 | 将方法调用者与被调用者分离,减少依赖,提高模块化。 | 不同模块间通过委托通信,降低耦合度。 |
6.2.3.多播委托
多播委托可以同时引用多个方法,并依次调用这些方法。
public delegate void MyDelegate(string message);
public class Program
{
public static void ShowMessage(string message)
{
Console.WriteLine("Message: " + message);
}
public static void LogMessage(string message)
{
Console.WriteLine("Log: " + message);
}
public static void Main()
{
MyDelegate del = ShowMessage; // 添加第一个方法
del += LogMessage; // 添加第二个方法
del("Hello, Multi-cast Delegates!"); // 依次调用 ShowMessage 和 LogMessage
}
}
// 输出:
// Message: Hello, Multi-cast Delegates!
// Log: Hello, Multi-cast Delegates!
6.2.4.内置委托
C# 提供了几个常用的内置委托,简化了委托的使用:
委托类型 | 描述 | 示例 |
---|---|---|
Action | 不返回值,有 0 到多个参数。 | Action action = ShowMessage; action(“Hello, Action!”); |
Func | 有返回值,有 0 到多个参数。 | Func<int, int, int> func = (a, b) => a + b; int result = func(3, 4); // 返回 7 |
Predicate | 返回 bool,有一个参数。 | Predicate predicate = x => x > 10; bool isGreater = predicate(15); // 返回 true |
using System;
public class Program
{
public static void Main()
{
// 使用 Action 示例
Action<string> action = ShowMessage;
action("Hello, Action!"); // 输出: Hello, Action!
// 使用 Func 示例
Func<int, int, int> func = (a, b) => a + b;
int result = func(3, 4); // 返回 7
Console.WriteLine("Func result: " + result); // 输出: Func result: 7
// 使用 Predicate 示例
Predicate<int> predicate = x => x > 10;
bool isGreater = predicate(15); // 返回 true
Console.WriteLine("Predicate result: " + isGreater); // 输出: Predicate result: True
}
// ShowMessage 方法
public static void ShowMessage(string message)
{
Console.WriteLine(message);
}
}
6.3.匿名方法
在开始正文之前,我先声明一点:现在C#中已经很少用匿名方法了,匿名方法是微软早期的产物,现在基本上都在用Lambda表达式了,如果你不想学,就跳过这一章吧,我平常也不用,主要是考虑到文章的完整性而写的这一章内容。
6.3.1.什么是匿名方法?
匿名方法是 C# 中无需命名、直接内联使用的临时方法。说人话:匿名方法也是一种方法,只不过没有名字而已。
6.3.2.匿名方法的用法
匿名方法的标准语法如下,其中,delegateType 是委托类型,如 Func 或 Action,delegateInstance 是委托的实例名称,parameters 是方法的参数列表。
delegateType delegateInstance = delegate(parameters) {
// 代码区
};
以下是一个使用匿名方法计算两个整数和的例子,在这个例子中,匿名方法 delegate(int x, int y) 计算了两个整数的和,并将结果返回给委托 add。
Func<int, int, int> add = delegate(int x, int y) {
return x + y;
};
int result = add(3, 4); // result 为 7
匿名方法也可以用于事件处理器,在下面这个例子中,匿名方法直接作为 Click 事件的处理器,当按钮被点击时显示一条消息。
button.Click += delegate(object sender, EventArgs e) {
MessageBox.Show("Button clicked!");
};
6.3.3.匿名方法与 Lambda 表达式的对比
匿名方法使用delegate 关键字,而Lambda 表达式通过使用( ) => 语法来定义参数和方法体。相比匿名方法,Lambda 表达式更短、更直观。
//lambda表达式
Func<int, int, int> add = (x, y) => x + y;
//匿名方法
Func<int, int, int> add = delegate(int x, int y) {
return x + y;
};
这一章的内容比较少,我也不想写太多,大家知道有这么个概念即可,实在没必要深究。
6.4.Lambda 表达式
6.4.1.什么是Lambda表达式?
Lambda表达式也是一种匿名方法(不了解匿名方法的小伙伴,去看我的上一篇文章:学一下匿名方法了),既然是匿名方法肯定也就是方法,所以Lambda表达式本质就是一种方法,它们的关系如图所示。
既然已经有了方法和匿名方法了,为什么还要弄一个Lambda表达式呢?其实人都有一个通病:懒。站着的时候想坐着,坐着又想躺着,程序员更是人群中懒和邋遢的代表,所以Lambda表达式就是为了方便程序员偷懒而设计的,但这个懒偷的好。Lambda表达式用来快速定义没有名字的小函数,尤其是在处理事件、回调函数和LINQ查询时,可以让代码更短更易读。
6.4.2.Lambda表达式语法
(1)语法结构
*语法结构* | *说明* |
---|---|
(parameters) => expression | 左侧是参数列表(入参),右侧是表达式或代码块 |
(parameters) => { statements } | 如果方法体包含多行代码,则使用大括号 {} 包裹 |
(2)具体用法
*示例* | *说明* | |
---|---|---|
单参数 | n => n % 2 == 0 | 当入参只有一个,无需小括号(也可以用小括号) |
多参数 | (a, b) => a + b | 当入参有两个及以上,参数列表要用小括号包裹 |
无参数 | () => Console.WriteLine(“Hello”) | 无入参时直接使用 () |
多行代码块 | (a, b) => { int sum = a + b; return sum; } | 方法体中有多行代码需用 {}包裹,有返回值用 return 返回结果。 |
using System;
using System.Linq; // 引入用于LINQ查询的命名空间
// 单参数:使用Lambda表达式筛选数组中的偶数
int[] numbers = { 1, 2, 3, 4, 5, 6 };
var evenNumbers = numbers.Where(n => n % 2 == 0);// 当入参只有一个,无需小括号(也可以用小括号),参数n没有用小括号
// 多参数:使用Lambda表达式创建一个加法函数
Func<int, int, int> add = (a, b) => a + b;// 当入参有两个及以上,参数列表要用小括号包裹,参数a和b被小括号包裹
int result = add(3, 4);
// 无参数:使用Lambda表达式创建一个无参数的方法
Action sayHello = () => Console.WriteLine("Hello, World!");// 无入参时直接使用 `()`
sayHello();
// 多行代码块:计算两个数的和并返回结果
Func<int, int, int> calculateSum = (a, b) =>
{// 方法体中有多行代码需用 `{}` 包裹,有返回值用 `return` 返回结果
int sum = a + b;
Console.WriteLine($"The sum of {a} and {b} is {sum}");
return sum;
};
int sumResult = calculateSum(10, 20);
// 事件处理:创建一个按钮并处理点击事件
var button = new Button();
button.Text = "Click Me";
button.Click += (sender, e) =>
{
MessageBox.Show("Button clicked!");// 使用Lambda表达式处理事件,并用 `MessageBox.Show` 显示消息
};
button.PerformClick();// 模拟按钮点击(仅用于演示)
6.4.3.Lambda表达式与匿名方法的对比
Lambda表达式的写法更简洁,省略了 delegate 关键字和参数类型声明,并且可以自动推断参数类型,提升了代码的可读性。
*匿名方法* | *Lambda表达式* |
---|---|
delegate(int a, int b) { return a + b; } | (a, b) => a + b |
delegate { Console.WriteLine(“Hi”); } | () => Console.WriteLine(“Hi”) |
7.泛型编程
7.1.泛型编程概述
7.1.1.什么是泛型编程?
generic这个单词的意思是:一般的,通用的,在编程中就被翻译成泛型的。你可能还是不能直观的理解理解这个单词的意思,我们举一个例子:假设你是一位厨师,你可能需要切洋葱、切土豆、切胡萝卜。如果每种食材都需要用不同的刀具,那你的厨房里就会充满各种各样的刀具,既繁琐又不方便管理。现在,想象你有一把万能刀,这把刀可以切任何食材,不论是洋葱、土豆还是胡萝卜,只需调整刀的设定就可以了。这样一来,你只需要一把刀就能完成所有的工作,这把菜刀就可以形容为通用的,万能的。
所谓泛型编程,就是编写可以重复使用的代码,以适用多种类型的数据。假设你写了一个函数来处理整数,然后又写了一个几乎一模一样的函数来处理字符串,这样的代码既冗余又难以维护,而泛型编程就能很好的解决这个问题,你只需编写一次代码,就可以让它适用于多种不同的数据类型。
7.1.2.泛型编程优点
- 减少重复代码:不用为每种数据类型编写一套代码,减少了代码量,也降低了出错的风险。
- 提高代码的通用性:一段泛型代码可以处理多种类型的数据,让代码更具通用性。
- 增强代码的安全性:使用泛型时,编译器可以检查类型是否正确,这样可以避免很多潜在的错误。
- 优化性能:泛型编程在编译时会生成针对具体类型的代码,没有装箱和拆箱的过程,不会影响运行时的性能。
7.1.3.泛型编程使用场景
(1)数据容器:像列表、字典、队列这样的数据结构可以使用泛型,这样你可以用同一个容器类来存储不同类型的数据。
// 使用泛型列表来存储整数
List<int> intList = new List<int>();
intList.Add(1);
intList.Add(2);
intList.Add(3);
Console.WriteLine("Integer List:");
foreach (int item in intList)
{
Console.WriteLine(item);
}
(2)通用算法:排序、搜索等算法可以用泛型来实现,这样它们就能处理任何类型的集合数据,而不局限于某一种类型。
// 泛型排序方法
void SortList<T>(List<T> list) where T : IComparable<T>
{
list.Sort(); // 使用内置的排序方法
}
// 创建并排序整数列表
List<int> intList = new List<int> { 3, 1, 4, 1, 5 };
SortList(intList);
Console.WriteLine("Sorted Integers: " + string.Join(", ", intList));
// 创建并排序字符串列表
List<string> stringList = new List<string> { "pear", "apple", "orange" };
SortList(stringList);
Console.WriteLine("Sorted Strings: " + string.Join(", ", stringList));
(3)接口和类:泛型可以让接口和类更灵活,能够处理多种类型的输入和输出。
// 使用泛型接口和类
var intBox = new Box<int>();
intBox.Put(42);
Console.WriteLine(intBox.Get()); // 输出: 42
var stringBox = new Box<string>();
stringBox.Put("Hello");
Console.WriteLine(stringBox.Get()); // 输出: Hello
// 定义一个泛型接口
interface IBox<T>
{
void Put(T item);
T Get();
}
// 实现泛型接口的类
class Box<T> : IBox<T>
{
private T _item;
public void Put(T item)
{
_item = item;
}
public T Get()
{
return _item;
}
}
7.2.泛型类和泛型方法
泛型类和泛型方法是在定义时不指定具体类型,而是在编译时传入类型参数,使它们能够在类型安全的前提下灵活处理多种数据类型。
我们用人话来解释一下上面这句话:泛型类本质就是类,只不过在实例化泛型类时,需要带着一个类型参数;泛型方法呢本质就是方法,只不过在调用泛型该方法时,也需要带着一个类型参数。
7.2.1.泛型类
泛型类是在类名后面加上尖括号 ,其中 T 是类型参数,可以是任何符号或字母,代表类可以操作的某种类型。
// 实例化泛型类并使用
var intInstance = new GenericClass<int>();//实例化泛型类用<>括号携带类型参数int
intInstance.SetValue(42);
Console.WriteLine(intInstance.GetValue()); // 输出: 42
var stringInstance = new GenericClass<string>();//实例化泛型类用<>括号携带类型参数string
stringInstance.SetValue("Hello, World!");
Console.WriteLine(stringInstance.GetValue()); // 输出: Hello, World!
// 定义泛型类
public class GenericClass<T>
{
private T _value;
public void SetValue(T value)
{
_value = value;
}
public T GetValue()
{
return _value;
}
}
7.2.2.泛型方法
泛型方法是在方法名之前加上尖括号 ,其中 T 是类型参数,表示方法可以操作的某种类型。注意:并不是只有泛型类中可以定义泛型方法,普通类中也可以定义泛型方法。
// 使用泛型方法
int x = 1, y = 2;
Swap(ref x, ref y);
Console.WriteLine($"x: {x}, y: {y}"); // 输出: x: 2, y: 1
string str1 = "first", str2 = "second";
Swap(ref str1, ref str2);
Console.WriteLine($"str1: {str1}, str2: {str2}"); // 输出: str1: second, str2: first
// 定义一个泛型方法
static void Swap<T>(ref T a, ref T b)
{
T temp = a;
a = b;
b = temp;
}
7.2.3.使用场景
7.3.泛型接口
7.3.1.什么是泛型接口?
泛型接口是指在定义时不指定具体数据类型,而是在实现或使用时传入具体类型,目的是让接口更通用,适用于不同类型的数据,从而提高代码的重用性和灵活性。通过泛型接口,可以避免为每种数据类型单独定义接口。
7.3.2.泛型接口用法
定义泛型接口的语法与泛型类类似。接口名后面的尖括号 表示这是一个泛型接口,T 是一个占位符,表示数据的类型。
下面的例子定义了一个通用的存储接口 IRepository,可以处理任意类型的 T,如 int、string,或者自定义的对象。
// 定义泛型接口
public interface IRepository<T>
{
void Add(T item);
T Get(int id);
void Delete(T item);
}
// 实现泛型接口,指定为 int 类型
public class IntRepository : IRepository<int>
{
private List<int> _items = new List<int>();
public void Add(int item)
{
_items.Add(item);
}
public int Get(int id)
{
return _items[id];
}
public void Delete(int item)
{
_items.Remove(item);
}
}
// 实现泛型接口,指定为 string 类型
public class StringRepository : IRepository<string>
{
private List<string> _items = new List<string>();
public void Add(string item)
{
_items.Add(item);
}
public string Get(int id)
{
return _items[id];
}
public void Delete(string item)
{
_items.Remove(item);
}
}
// 使用泛型接口
class Program
{
static void Main(string[] args)
{
IRepository<int> intRepo = new IntRepository();
intRepo.Add(10);
Console.WriteLine(intRepo.Get(0)); // 输出: 10
IRepository<string> stringRepo = new StringRepository();
stringRepo.Add("Hello");
Console.WriteLine(stringRepo.Get(0)); // 输出: Hello
}
}
7.3.3.泛型接口的使用场景
接口 | 描述 | 使用场景 |
---|---|---|
IEnumerable | 表示可枚举的集合,支持 foreach 遍历操作。 | 适用于所有集合类型,如 List、Array。 |
IComparable | 定义对象的比较规则,使对象能够与其他对象进行比较。 | 用于排序、对象比较等场景,支持自定义比较逻辑。 |
IComparer | 提供外部比较器,定义比较两个对象的规则。 | 常用于自定义排序逻辑,如 List.Sort()。 |
7.4. 泛型委托
接口 | 描述 | 使用场景 |
---|---|---|
IEnumerable | 表示可枚举的集合,支持 foreach 遍历操作。 | 适用于所有集合类型,如 List、Array。 |
IComparable | 定义对象的比较规则,使对象能够与其他对象进行比较。 | 用于排序、对象比较等场景,支持自定义比较逻辑。 |
IComparer | 提供外部比较器,定义比较两个对象的规则。 | 常用于自定义排序逻辑,如 List.Sort()。 |
7.4.1.什么是泛型委托?
泛型委托是指在定义时不指定具体数据类型,而是在使用时传入具体类型的委托。它允许我们编写通用的委托,可以在不同类型的数据之间重用。
7.4.2.泛型委托用法
定义泛型委托的语法与泛型类和泛型接口类似。委托名后面的尖括号 表示这是一个泛型委托,T 是一个占位符,表示数据的类型。
下面的例子定义了一个通用的委托 MyDelegate,可以处理任意类型的 T,如 int、string,或者自定义的对象。
class Program
{
// 定义泛型委托
public delegate void MyDelegate<T>(T item);
static void Main(string[] args)
{
// 使用泛型委托,传递类型参数int
MyDelegate<int> intDelegate = (int item) =>
{
Console.WriteLine($"Int: {item}");
};
// 调用委托
intDelegate(42); // 输出: Int: 42
}
}
泛型委托不仅可以用于单个类型,还可以处理多个类型参数,例如处理多个输入参数或返回值的委托,以下是定义具有两个泛型参数的委托的示例,MyFunc<T1, T2, TResult> 是一个带有两个输入类型和一个返回类型的泛型委托。通过这种方式,我们可以处理更加复杂的场景,例如根据多个输入生成一个返回值。
// 使用MyFunc<T1, T2, TResult>
MyFunc<int, string, string> func = Combine;//这里T1是int,T2是string,TResult是string
string result = func(100, "Hello");
Console.WriteLine(result); // 输出: 100 - Hello
// 定义 Combine 方法
string Combine(int number, string text)
{
return $"{number} - {text}";
}
// 定义一个带有两个泛型参数的委托
public delegate TResult MyFunc<T1, T2, TResult>(T1 item1, T2 item2);
7.4.3.泛型委托的使用场景
委托 | 描述 | 使用场景 |
---|---|---|
Action | 不返回值的泛型委托,接受一个参数。 | 适用于执行某种操作,不需要返回结果。 |
Func<T, TResult> | 返回值的泛型委托,接受多个参数并返回一个结果。 | 适用于需要根据输入返回结果的场景,例如计算函数。 |
Predicate | 返回布尔值的泛型委托,用于判断条件。 | 适用于条件判断和筛选操作。 |
// 1. Action<T> 示例:不返回值的泛型委托,接受一个参数
Action<string> printMessage = (string message) =>
{
Console.WriteLine($"Message: {message}");
};
printMessage("Hello, World!"); // 输出: Message: Hello, World!
// 2. Func<T, TResult> 示例:返回值的泛型委托,接受多个参数并返回一个结果
Func<int, int, int> addNumbers = (int x, int y) =>
{
return x + y;
};
int sum = addNumbers(10, 20);
Console.WriteLine($"Sum: {sum}"); // 输出: Sum: 30
// 3. Predicate<T> 示例:返回布尔值的泛型委托,用于判断条件
Predicate<int> isEven = (int number) =>
{
return number % 2 == 0;
};
bool result = isEven(10);
Console.WriteLine($"Is 10 even? {result}"); // 输出: Is 10 even? True
// 结合 Predicate 和 List.FindAll 方法,筛选出偶数
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
List<int> evenNumbers = numbers.FindAll(isEven);
Console.WriteLine("Even numbers:");
foreach (int number in evenNumbers)
{
Console.WriteLine(number); // 输出: 2, 4, 6, 8, 10
}
7.5. 泛型集合
已经在集合那一章节写完。
7.6. 泛型约束
7.6.1.什么是泛型约束?
泛型约束是指在定义泛型类或泛型方法时,通过关键字where来限制类型参数的范围。
什么是类型参数的范围呢?类型参数的范围是指泛型类、方法或委托中,类型参数必须满足的条件或限制,例如继承自某个类、实现某个接口、是引用类型、值类型或具有无参构造函数等,这些限制通过泛型约束 where 关键字指定。
7.6.2.泛型约束用法
定义泛型约束的语法与定义泛型基本相同,只需要在类型参数后面通过 where 关键字来指定约束条件。常见的约束类型包括继承约束、接口约束、构造函数约束等。
下面的例子定义了一个使用泛型约束的类 Repository,其中 T 必须继承自 Entity,并且必须有一个无参构造函数,如果不满足这两个条件编译器就会报错 。
// 定义基类 Entity
public class Entity
{
public int Id { get; set; }
}
// 定义泛型类 Repository<T>,带有两个约束:T 必须继承自 Entity 并且有无参构造函数
public class Repository<T> where T : Entity, new()
{
public T Create()
{
return new T(); // 这里要求 T 必须有无参构造函数
}
}
// 定义符合约束的类 ValidEntity,有无参构造函数
public class ValidEntity : Entity
{
public ValidEntity()
{
Id = 1;
}
}
// 定义不符合约束的类 InvalidEntity,没有无参构造函数
public class InvalidEntity : Entity
{
public InvalidEntity(int id)
{
Id = id;
}
}
class Program
{
static void Main(string[] args)
{
// 使用符合约束的类 ValidEntity
Repository<ValidEntity> validRepo = new Repository<ValidEntity>();
ValidEntity validEntity = validRepo.Create();
Console.WriteLine($"ValidEntity ID: {validEntity.Id}"); // 输出: ValidEntity ID: 1
// 使用不符合约束的类 InvalidEntity 会导致编译错误
// Repository<InvalidEntity> invalidRepo = new Repository<InvalidEntity>();
// InvalidEntity invalidEntity = invalidRepo.Create(); // 无法编译,InvalidEntity 缺少无参构造函数
// 编译错误提示:
// "InvalidEntity" 没有无参构造函数,无法满足 new() 约束。
}
}
7.6.3.常见的泛型约束类型
(1)类约束 :限制类型参数必须是某个类的子类。适用于当你希望类型参数继承某个类并复用该类的功能时。
// 定义一个基类
public class Animal
{
public string Name { get; set; }
public void Speak()
{
Console.WriteLine($"{Name} makes a sound.");
}
}
// 定义一个带有类约束的泛型类,T 必须继承自 Animal
public class AnimalHouse<T> where T : Animal
{
public void Welcome(T animal)
{
animal.Speak(); // 可以调用基类 Animal 的方法
}
}
class Program
{
static void Main()
{
AnimalHouse<Animal> house = new AnimalHouse<Animal>();
Animal dog = new Animal { Name = "Dog" };
house.Welcome(dog); // 输出: Dog makes a sound.
}
}
(2)接口约束 :限制类型参数必须实现某个接口。适用于当你希望类型参数实现某些方法或属性时。
// 定义一个接口
public interface IRun
{
void Run();
}
// 实现接口的类
public class Person : IRun
{
public void Run()
{
Console.WriteLine("Person is running");
}
}
// 定义一个带有接口约束的泛型类,T 必须实现 IRun 接口
public class Race<T> where T : IRun
{
public void Start(T runner)
{
runner.Run(); // 可以调用 IRun 接口中的方法
}
}
class Program
{
static void Main()
{
Race<Person> race = new Race<Person>();
Person person = new Person();
race.Start(person); // 输出: Person is running
}
}
(3)构造函数约束 :限制类型参数必须有一个无参构造函数。这常用于当你需要在泛型类或方法中创建类型实例时。
// 定义一个带有构造函数约束的泛型类,T 必须有无参构造函数
public class Factory<T> where T : new()
{
public T CreateInstance()
{
return new T(); // 可以安全地创建 T 类型的实例
}
}
// 使用该类
class Program
{
static void Main()
{
Factory<Person> factory = new Factory<Person>();
Person person = factory.CreateInstance();
Console.WriteLine("Instance of Person created.");
}
}
// Person 类有一个无参构造函数
public class Person
{
public string Name { get; set; }
}
(4)值类型约束 :限制类型参数必须是值类型。这在处理数值或结构体时非常有用,因为值类型有更好的性能表现。
// 定义一个带有值类型约束的泛型类,T 必须是值类型
public class MathOperations<T> where T : struct
{
public T Add(T a, T b)
{
dynamic x = a; // 使用动态类型进行数学操作
dynamic y = b;
return x + y;
}
}
// 使用该类
class Program
{
static void Main()
{
MathOperations<int> mathOps = new MathOperations<int>();
int result = mathOps.Add(10, 20);
Console.WriteLine($"Result: {result}"); // 输出: Result: 30
}
}
(5)引用类型约束:限制类型参数必须是引用类型。这在你希望处理对象而非值类型时非常有用。
// 定义一个带有引用类型约束的泛型类,T 必须是引用类型
public class Repository<T> where T : class
{
private List<T> _items = new List<T>();
public void AddItem(T item)
{
_items.Add(item);
}
public T GetItem(int index)
{
return _items[index];
}
}
// 使用该类
class Program
{
static void Main()
{
Repository<Person> repo = new Repository<Person>();
repo.AddItem(new Person { Name = "John" });
Person person = repo.GetItem(0);
Console.WriteLine($"Retrieved Person: {person.Name}"); // 输出: Retrieved Person: John
}
}
public class Person
{
public string Name { get; set; }
}
7.6.4.泛型约束的使用场景
约束类型 | 描述 | 使用场景 |
---|---|---|
where T : class | 限制类型参数为引用类型。 | 适用于泛型类必须处理引用类型的场景,如对象操作。 |
where T : struct | 限制类型参数为值类型。 | 适用于泛型类处理数值类型时,确保性能和类型安全。 |
where T : new() | 限制类型参数必须具有无参构造函数。 | 适用于需要实例化泛型类型的场景。 |
where T : BaseClass | 限制类型参数必须是某个类的子类。 | 适用于泛型类型继承某个基类,复用基类功能的场景。 |
where T : InterfaceName | 限制类型参数必须实现某个接口。 | 适用于泛型类型必须实现接口,并使用接口方法的场景。 |
7.7.协变与逆变
这个知识点我有点没有理解透彻,先欠着吧。