概述
什么是C#
C#
,微软推出的,面向对象的,高级编程语言。
什么是DotNET
DotNET、.NET
,是一系列开发框架的总称。包含.NET Framework
(Windows)、.Net Core
(Windows、Linux和MacOS)、MAUI
(移动端跨平台)等。
在早期的资料中,.NET
通常是指.NET Framework
(Windows)。
现在,一般语境下,.NET
通常是指.Net Core
(Windows、Linux和MacOS)。
C#
和.Net
的关系,就像Java
和JDK
的关系、JavaScript
和Node.js
的关系。
Java
是一门编程语言,JDK
是可以运行Java
的框架。JavaScript
是一门编程语言,Node.js
是可以运行JavaScript
的框架。C#
是一门编程语言,.Net
是可以运行C#
的框架。
除了C#
,.Net
还支持Visual Basic
、C++
等语言。
环境搭建
Windows
Visual Studio
在Windows系统上,推荐Visual Studio
这个IDE。
官网:https://visualstudio.microsoft.com
安装
在安装的时候,对于C#场景,建议选择如下"工作负荷":
修改字体
我个人认为Visual Studio 2022
默认的字体很别扭,可以参考如下步骤修改字体。
- 在菜单栏上,选择
工具
>选项
。 - 在选项列表中,选择
环境
>字体和颜色
。
可以考虑这个字体:Yahei Consolas Hybrid
创建项目
控制台项目
创建控制台项目,步骤如下:
- 创建新项目。
- 选择
控制台应用(.NET)
- 设置项目名称、存储位置、解决方案名、框架。
至此,控制台项目,创建完成。
示例代码:
1 | using System; |
点击顶部的ConsoleApp
,启动。
运行结果:
Winform项目
创建一个Winform项目
。
创建完成后,默认左侧没有"工具箱"的Tab,需要在菜单栏中依次选择视图
> 工具箱
,打开工具箱Tab。
MacOS
配置
具体可以参考微软官方教程:
https://learn.microsoft.com/zh-cn/dotnet/core/install/macos
关键步骤如下:
- 下载并安装
.NET SDK
- 为Visual Studio Code,安装插件
C# Dev Kit
。
安装完成后,需要关闭Visual Studio Code,然后重新打开。
https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csdevkit
使用
插件C# Dev Kit
安装完成后,会在Visual Code的主页左侧看到,创建Create .NET Project
,创建项目。
可以点击右上角,运行项目。
Linux
说明
在Linux上的步骤,和在MacOS类似,推荐的IDE也是Visual Code。
不过,一般不会在Linux上开发基于C#语言的程序。
但是,常有在Linux上的部署事宜。
本文讨论在Linux上的部署。
安装DotNET
以Rocky这个发行版为例,安装命令如下:
1 | dnf install dotnet-sdk-8.0 |
在有些系统下,可能需要先安装对应的源,步骤如下:
- 安装源:
1
rpm -Uvh https://packages.microsoft.com/config/centos/8/packages-microsoft-prod.rpm
- 安装
dotnet-sdk-8.0
:1
yum install dotnet-sdk-8.0
打包
Windows
在Windows上,基于Visual Studio,可以通过GUI界面进行打包。
右键选择发布:
选择发布路径等:
MacOS
在MacOS上,通过dotnet publish
,打包发布到本地。
示例代码:
1 | dotnet publish |
运行结果:
1 | Determining projects to restore... |
部署
需要把整个publish
文件夹的内容,都上传到Linux服务器上。
例如:
1 | ConsoleApp1 |
然后通过dotnet
命令启动。
示例代码:
1 | dotnet ConsoleApp1.dll |
运行结果:
1 | Hello, World! |
Remote-SSH
什么是Remote-SSH
本质上,和我们通过向日葵,远程一台电脑,进行开发,没有区别。
- 代码在远程的主机上。
- DotNet环境也是远程主机上的。
- 甚至我们在VSCode上安装的插件,也是为远程主机安装的。
搭建
安装插件Remote - SSH
、Remote Explorer
和Remote - SSH: Editing Configuration Files
。
修改本机.ssh\config
文件,新增如下内容:
1 | # 192.168.13.165 |
- 使用密码登录远程主机。
后续操作,和我们通过向日葵,远程一台电脑,进行开发,没有区别。
只是我们用了一种更友好的方式,更高级的方式,进行远程开发。
开始
例子
如下,是一段C#代码:
1 | using System; |
解释说明:
using
、namespace
、internal
、static
、class
、void
等,是关键词。using System;
,引入命名空间。Program
,类型,一个类包含一个或多个方法、属性、变量等。namespace WindowsFormsApp1
,当前程序的命名空间,默认与项目同名。//
,注释。- 方法名、方法体:在这里,Main方法是程序的入口。
1
2
3
4
5
6static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1());
}
注释
- 单行注释,以
//
开头。 - 多行注释,以
/*
开头,以*/
结尾,/*
和*/
之间的所有内容都属于注释内容。 - 文档注释,以
///
开头。
标识符
作用:用来为类、变量、函数或任何其他自定义内容命名。
定义规则:
- 以字母、下划线或@开头,后面可以跟一系列的字母、数字、下划线、@。
- 第一个字符不能是数字。
- 不包含任何嵌入的空格或其他符号。
- 不能是C#的关键字。
- 区分大小写。
- 不能与C#的类库名称相同。
关键字
关键字是编译器预先定义好的一些单词,这些关键字对编译器有特殊的意义,不能用作标识符。
C#中的关键字,分为保留关键字和上下文关键字。
上下文关键字,在代码的上下文中具有特殊的意义,例如get
和set
。一般来说,C#语言中新增的关键字都会作为上下文关键字,这样可以避免影响到使用旧版语言编写的C#程序。
保留关键字 | ||||||
---|---|---|---|---|---|---|
abstract | as | base | bool | break | byte | case |
catch | char | checked | class | const | continue | decimal |
default | delegate | do | double | else | enum | event |
explicit | extern | false | finally | fixed | float | for |
foreach | goto | if | implicit | in | in(genericmodifier) | int |
interface | internal | is | lock | long | namespace | new |
null | object | operator | out | out(genericmodifier) | override | params |
private | protected | public | readonly | ref | return | sbyte |
sealed | short | sizeof | stackalloc | static | string | struct |
switch | this | throw | true | try | typeof | uint |
ulong | unchecked | unsafe | ushort | using | virtual | void |
volatile | while |
上下文关键字 | ||||||
---|---|---|---|---|---|---|
add | alias | ascending | descending | dynamic | from | get |
global | group | into | join | let | orderby | partial(type) |
partial(method) | remove | select | set |
数据类型
分类
C#中的数据类型可以分为三类:
- 值类型
Value types
- 引用类型
References types
- 指针类型
Pointer types
为了类型安全,C#默认不支持指针。除非使用unsafe关键词并开启不安全代码(unsafe code)开发模式。
值类型
什么是值类型
和Java中的基本数据类型没有明显区别,包括内存结构都是相似的,只是名称不同。
分类
- 整型
- 浮点型
- 布尔型
- 字符型
还有一类值类型,结构体,这个是Java中没有的,会在下文讨论"类和对象"的时候,进行讨论。
整型
概述
根据存储容量大小,可以分为不同的类型。
根据是否支持负数,可以分为有符号和无符号两种。
例如:
- 有符号整数:
sbyte
、short
、int
、long
- 无符号整数:
byte
、ushort
、uint
、ulong
与Java的区别
浮点型
在Java中,浮点型有float
(单精度)、double
(双精度)两种。
而C#中,浮点型有三种:
float
,单精度double
,双精度decimal
,高精度,精确数字。
与在Java中一样,默认的浮点类型是double
,所以:
- 定义
float
类型,以f
或F
结尾。1
float f1 = 1.23f;
- 定义
decimal
类型,以m
或M
结尾。1
decimal money = 12.34m;
布尔型
与Java的布尔型没有明显区别。
字符型
与Java的字符型没有明显区别。
引用类型
什么是引用类型
引用类型,与Java中的引用类型几乎一致。
也有栈内存、堆内存的概念。
C#中内置的引用类型包括:
Object
,对象Dynamic
,动态string
,字符串
对象类型
与在Java中一样,Object,对象类型,是所有数据类型的最终基类。
动态类型
什么是动态类型
动态类型,Dynamic。
动态类型定义的变量,可以存储任何类型的值。
与对象类型的区别
动态类型,似乎和对象类型一样?我们也可以用对象类型的存储任何类型的值。
示例代码:
1 | object c = "Hello, World!"; |
区别在于类型检查方面:
- 对象类型变量的类型检查是在编译时进行的
- 动态类型变量的类型检查是在程序运行时进行的。
字符串类型
两种定义方式
字符串类型,string,
有两种定义字符串类型的方式,分别是:
" "
@" "
,逐字字符串,会自动进行将转义的字符串。
常用属性
this[Int32]
:获取指定位置处字符。Length
:获取当前String对象中的字符数(字符串的长度)。
常用方法
方法 | 描述 |
---|---|
Concat(String,String) |
连接两个指定的字符串 |
Contains(String) |
判断一个字符串中是否包含另一个字符串 |
Copy(String) |
将字符串的值复制一份,并赋值给另一个字符串 |
EndsWith(String) |
用来判断字符串是否以指定的字符串结尾 |
Format(String,Object) |
将字符串格式化为指定的字符串表示形式 |
IndexOf(String) |
返回字符在字符串中的首次出现的索引位置,索引从零开始 |
Insert(Int32,String) |
在字符串的指定位置插入另一个字符串,并返回新形成的字符串 |
IsNullOrEmpty(String) |
判断指定的字符串是否为空(null)或空字符串(“”) |
IsNullOrWhiteSpace(String) |
判断指定的字符串是否为null、空或仅由空白字符组成 |
Join(String,String[]) |
串联字符串数组中的所有元素,并将每个元素使用指定的分隔符分隔开 |
LastIndexOf(Char) |
获取某个字符在字符串中最后一次出现的位置 |
Remove(Int32) |
返回一个指定长度的新字符串,将字符串中超出长度以外的部分全部删除 |
Replace(String,String) |
使用指定字符替换字符串中的某个字符,并返回新形成的字符串 |
Split(Char[]) |
按照某个分隔符将一个字符串拆分成一个字符串数组 |
StartsWith(String) |
判断字符串是否使用指定的字符串开头 |
Substring(Int32) |
从指定的位置截取字符串 |
ToCharArray() |
将字符串中的字符复制到Unicode字符数组 |
ToLower() |
将字符串中的字母转换为小写的形式 |
ToUpper() |
将字符串中的字母转换为大写形式 |
Trim() |
删除字符串首尾的空白字符 |
类型转换
分类
与Java中一样,在C#中有两种形式的类型转换方式:
- 隐式类型转换,也被称为自动类型转换。
- 显式类型转换,也被称为强制类型转换。
隐式类型转换
与Java中的隐式类型转换没有明显区别。
都是把一个表示数据范围小的数值或者变量赋值给另一个表示数据范围大的变量。
这种转换方式是自动的,隐式的。
1 | short sValue = 123; |
显式类型转换
什么是显式类型转换
把一个表示数据范围大的数值或者变量赋值给另一个表示数据范围小的变量。
这种转换方式是强制的,显式的,需要明确指出。
且,可能会造成数据丢失。
显式类型转换,有四种方式:
(type)value
type.Parse(【string】)
type.TryParse(【string】, out type 变量)
Convert.ToType(value)
(type)value
在下例中,转换前的值是3.56
,转换后的值为3
,造成了数据丢失。
示例代码:
1 | double dValue = 3.56; |
运行结果:
1 | 3 |
type.Parse(string)
将字符串转换为对应的值类型,注意入参只能是字符串,且必须是符合规范的字符串。
字符串"3.56"
转成float类型的值,是成功的。示例代码:
1 | float val = float.Parse("3.56"); |
运行结果:
1 | 3.56 |
但是,字符串"3.56"
,直接转成int类型的值,会失败。示例代码:
1 | int val = int.Parse("3.56"); |
运行结果:
1 | Unhandled exception. System.FormatException: The input string '3.56' was not in a correct format. |
type.TryParse(string,out type 变量)
type.TryParse(string,out type 变量)
,返回布尔值,表示转换成功或失败。而接收值,作为out type 变量
传入。
示例代码:
1 | string str = "12.89"; |
运行结果:
1 | 0 |
Convert.ToType(value)
Convert,C#内置的一种工具类。
示例代码:
1 | // 转换为byte |
Convert中内置了一系列的类型转换方法,本文不一一列举。
运算符
分类
与Java中一样,常见的运算符也可以分为如下七类:
- 算数运算符
- 字符串连接运算符
- 赋值运算符
- 自增自减运算符关系运算符
- 逻辑运算符
- 短路逻辑运算符
- 三元运算符
算术运算符
与Java中的算术运算符没有明显区别。
字符串连接运算符
与Java中的字符串连接运算符没有明显区别。
赋值运算符
与Java中的赋值运算符没有明显区别,也隐含了强制类型转换。
示例代码:
1 | short s = 10; |
运行结果:
1 | 20 |
自增自减运算符
与Java中的自增自减运算符没有明显区别。
- 单独使用的时候:
++
和--
无论是放在变量的前边还是后边,结果是一样的。 - 参与操作的时候:
- 如果放在变量的后边,先拿变量参与操作,后拿变量做
++
或者--
。 - 如果放在变量的前边,先拿变量做
++
或者--
,后拿变量参与操作。
- 如果放在变量的后边,先拿变量参与操作,后拿变量做
关系运算符
与Java中的关系运算符没有明显区别。
逻辑运算符
与Java中的逻辑运算符没有明显区别。
短路逻辑运算符
与Java中的短路逻辑运算符没有明显区别。
三元运算符
与Java中的三元运算符没有明显区别。
流程控制
条件分支
if结构
与Java中的if结构没有明显区别。
switch结构
去Java的区别
- 数据类型支持:
- Java:
switch
表达式支持byte
,short
,char
,int
、枚举类型和字符串。 - C#:除了支持类似的数据类型外,还支持
long
类型。
- Java:
- 模式匹配:
基于模式匹配,C#中的switch
语句可以用于更加复杂的条件判断,而不仅仅是简单的相等比较。
模式匹配
示例代码:
1 | object value = "Hello, world!"; |
运行结果:
1 | Long string: Hello, world! |
循环分支
for循环
与Java中的for循环没有明显区别。
while循环
与Java中的while循环没有明显区别。
do…while
与Java中的do…while循环没有明显区别。
foreach
foreach
,用于遍历数组或者集合对象中的每一个元素。
语法格式:
1 | foreach(数据类型 变量名 in 数组或集合对象) |
示例代码:
1 | string[] names = new string[] { "Lily","Lucy","Jason","Steven","White" }; |
运行结果:
1 | Lily |
对应Java中的"增强for循环",语法格式如下:
1 | for(元素数据类型 变量名 : 数组/集合对象名) { |
数组
基本操作
和数组的基本操作相关的重要内容有:
- 数组初始化
- 静态初始化
- 动态初始化
- 数组元素访问
- 内存结构
这些和Java中数组的基本操作没有明显区别。
多维数组
声明多维数组
1 | // 声明一个二维数组 |
初始化
指定维度
1 | int[,] arr = new int[3,2]{ |
不指定维度
1 | int[,] arr = new int[,]{ |
直接赋值,不用new
1 | int[,] arr = { |
访问
使用arr[i,j]
的形式来访问二维数组中的每个元素。
在下例中,我们遍历数组中每一个元素,示例代码:
1 | int[,] arr = new int[3,4] |
GetLength(i)
获取指定维中的元素个数。
与Java中多维数组的区别
- 语法存在区别。
- 在Java中,多维数组实际上是一维数组的一维数组(即数组的数组),因此每一行可以有不同的长度(称为"锯齿数组"或"非矩形数组")。
在下文的Java语言的例子,arr的第二个元素,长度为3,而其他的都是4。这在Java中是被允许的。
1 | int[][] arr = { |
但是在C#中,这不被允许:
Array类
简介
Array
类是C#中所有数组的基类,提供了一系列用来处理数组的操作。
常用属性
Length
:获取System.Array的所有维度中的元素总数。LongLength
:表示System.Array的所有维数中元素的总数,一个64位整数。
常用方法
Copy(Array,Array,Int32)
:从第一个元素开始拷贝数组中指定长度的元素,并将其粘贴到另一个数组(从第一个元素开始粘贴),使用32位整数来指定要拷贝的长度。GetLength()
:获取数组指定维度中的元素数,并返回一个32位整数。IndexOf(Array,Object)
:在一个一维数组中搜索指定对象,并返回其首个匹配项的索引。Reverse(Array)
:反转整个一维数组中元素的顺序。Sort(Array)
:对一维数组中的元素排序。
方法
什么是方法
与Java的方法,没有明显区别,包括其组成部份都是一样的,由以下部分组成:
- 访问权限修饰符
- 返回值类型
- 方法名称
- 参数列表
- 方法主体
语法格式
与Java中声明方法的语法格式没有明显区别,如下:
1 | 访问修饰符 返回值类型 方法名(参数列表) |
参数传递
与Java的区别
在C#中,有三种参数传递方式:
- 值传递(传递基本类型)
- 引用传递(传递引用类型)
- 输出传递
其中值传递和引用传递,与Java中的没有明显区别。输出传递,是Java所没有的特性。
输出传递
输出传递,其参数用out
进行修饰。
示例代码:
1 | int a = 10; |
运行结果:
1 | 调用前:a=10 |
参数数组
参数数组,即参数个数可变,对应Java中的可变参数。
在语法方面存在差异,Java中可变参数的语法格式如下:
1 | 修饰符 返回值类型 方法名(数据类型... 变量名) { } |
在C#中,格式如下:
1 | 修饰符 返回值类型 方法名(params 类型名称[] 数组名称) {} |
使用参数数组时,调用方法时,既可以直接为方法传递一个数组作为参数,也可以使用函数名(参数1, 参数2, ..., 参数n)
的形式传递若干个具体的值。
示例代码:
1 | static void ShowMessage(params string[] infos) |
运行结果:
1 | Leah,Welcome,the class |
类和对象
类
语法格式
与Java中,类的语法格式,没有明显区别。
都是由成员变量和成员方法组成:
1 | public class 类名 { |
创建对象
C#中的对象、类和对象的关系、对象的创建方法,和Java中没有明显区别。
1 | 类名 对象名 = new 类名(); |
属性
什么是属性
在Java中,一种我们常见的操作,是将成员变量设置为private
,再用定义public
的getter
和setter
方法。
就这种操作规范,在C#中,被称为属性。
属性(Property):类(class)、结构体(structure)和接口(interface)都可以包含,使用访问器(accessors)可以进行读写的,私有的,成员变量。
访问器
什么是访问器
访问器,类似Java中,被public
修饰的getter
和setter
方法。
使用访问器
在C#中,使用访问器,在代码上,更精简。
示例代码:
1 | namespace ConsoleApp |
运行结果:
1 | 课程编号:101 |
索引器
什么是索引器
允许在访问器的get
和set
方法上,传入其他参数。
例如,我们可以基于索引器,实现对数组类型更精细的访问。
语法
1 | element-type this[int index] |
案例
示例代码:
1 | class IndexedNames |
运行结果:
1 | Zara |
重载索引器
索引器可以被重载,没有必要让索引器必须是整型的,可以是其他类型。
在下文的例子中,public string this[int index]
和public int this[string name]
,重载了索引器,采用了字符串类型的索引。
示例代码:
1 | class IndexedNames |
运行结果:
1 | 2 |
抽象类
与Java中的抽象类一样,C#中的抽样类也是使用abstract
关键字创建。
其规则也和Java中的抽象类一样:无法直接基于抽象类实例化,必须通过子类对象实例化。
示例代码:
1 | // 抽象类 |
运行结果:
1 | 编号:101,姓名:李红,年龄:25 在学校学习! |
解释说明:public abstract int Age { get; set; }
是抽象属性,在派生类Student
中实现。
静态类
什么是静态类
在上文讨论Convert
的时候,我们提到了静态类。
静态类,用static
关键字修饰,静态类不能被实例化,静态类中的成员也必须是静态的。
静态类一般用于封装通用处理类,里边封装相关的一系列通用方法,可重用。即,静态类一般都是"工具类"。
特点
- 只包含静态成员(静态成员变量、静态成员方法)。
包括,可以包含静态构造函数。 - 无法实例化。
- 无法派生子类。
- 不能包含实例构造函数。
密封类
密封类,使用sealed
关键字修饰,无法被继承。
构造函数
什么是构造函数
与Java中的构造函数一样。
在C#中,构造函数是与类(或结构体)具有相同名称的成员函数,当创建一个类的对象时会自动调用类中的构造函数。通常使用类中的构造函数来初始化类中的成员变量。
与Java中一样,注意:
- 构造方法的创建方面:
- 如果没有定义构造方法,系统将给出一个默认的无参数构造方法。
- 如果定义了构造方法,系统将不再提供默认的构造方法。
- 构造方法的重载:
- 如果自定义了带参构造方法,还要使用无参数构造方法,那怎么办呢?构造方法的重载,再写一个无参数构造方法。
在C#中,构造函数可以分为三种:
- 实例构造函数
被public
修饰的构造函数。 - 私有构造函数
被private
修饰的构造函数,防止外部类创建该类的实例。 - 静态构造函数
静态构造函数
静态构造函数用于初始化类的静态数据或执行一次性操作。它在类的第一个实例创建或静态成员首次被访问时自动调用。
静态构造函数的特点:
- 无访问修饰符和参数。
- 每个类只能有一个静态构造函数。
- 不能继承、重载或直接调用。
- 执行时间由CLR控制,且在实例构造函数之前运行。
在下文的例子中,我们会看到先执行静态构造函数,再执行实例构造函数。示例代码:
1 | public class Teacher |
运行结果:
1 | count = 3 |
结构体
什么是结构体
结构体,Struct,一种值类型(value type),用于组织和存储相关数据。
定义结构体
定义结构体,需要基于struct
关键词。
1 | struct Books |
示例代码:
1 | struct Books |
运行结果:
1 | Book 1 title : C Programming |
和类的区别
- 数据类型
结构是值类型(Value Type)
类是引用类型(Reference Type) - 继承
结构不能继承其他结构或类,也不能作为其他结构或类的基类。
类支持继承。 - 默认构造函数:
结构不能有无参数的构造函数。
类可以有无参数的构造函数。 - 赋值行为
结构变量在赋值时会复制整个结构,因此每个变量都有自己的独立副本。
类型为类的变量在赋值时存储的是引用,因此两个变量指向同一个对象。 - 传递方式:
结构对象通常通过值传递。
类型为类的对象在方法调用时通过引用传递。 - 可空性:
结构体是值类型,不能直接设置为null
(可以使用Nullable<T>
或称为T?
的可空类型)。
类的实例默认可以为null
。 - 性能和内存分配:
结构通常更轻量,在栈上分配内存,适用于简单的数据表示。
类涉及更多的内存开销和管理。
枚举
和Java中的枚举没有明显区别。
继承
什么是继承
和Java中的继承没有明显区别。
同样,只支持单继承,一个派生类只能继承一个基类;不支持多重继承,即不支持一个类同时继承多个基类;继承可以传递。
语法格式
在Java中,继承基于extends
关键字;在C#中,是:
。
1 | class 派生类:基类 |
多态
分类
- 静态多态,编译时多态
通过"方法重载"和"运算符重载"等实现编译时多态。 - 动态多态,运行时多态
通过方法重写实现的运行时多态。
静态多态
方法重载
在同一个作用域中,可以定义多个同名的方法,但是这些方法彼此之间必须有所差异,比如参数个数不同或参数类型不同等等,返回值类可以不同。
运算符重载
运算符重载基于operator
关键字后跟运算符的形式来定义的,我们可以将被重新定义的运算符看作是具有特殊名称的函数,与其他函数一样,该函数也有返回值类型和参数列表。
示例代码:
1 | public class StringNew |
运行结果:
1 | 42,63 |
动态多态
override和new
在C#中,子类"重写"父类的方法,有两种方式:
override
new
而在Java中,有且仅有一种方式,override
。
两种方式的区别在于:
override
、直译"推翻"、即子类直接推翻父类的方法,也就是所谓的"覆盖"、“重写”、“覆写”。new
、直译"新建",子类新建一个方法,子类和父类的方法同时存在。
具体可以看下面的例子。
Derived
继承了Base
,以override
的方式重写了Method1
,以new
的方式新建了Method2
;当Base baseClass= new Derived();
时,发生了一次向上转型,baseClass
从Derived
转型为其父类Base
类型;baseClass.Method1
调用的是被override
之后的Derived.Method1
;baseClass.Method2
调用的是Base.Method2
,因为baseClass.Method2
没有被影响到。
示例代码:
1 | public class Base |
运行结果:
1 | Derived's override function Method1 |
虚方法和抽象方法
虚方法
被virtual
修饰的方法。
对于虚方法,子类可以将其override
,也可以将其new
。
抽象方法
被abstract
修饰的方法。
特点:
- 只能在抽象类中定义,由抽象类的派生类中实现。
- 子类只能
override
抽象方法,不能new
抽象方法。 - 子类必须实现抽象方法。
区别
- 关键字
- 抽象方法:
abstract
- 虚方法:
virtual
- 抽象方法:
- 定义位置
- 抽象方法,必须在抽象类中。
- 虚方法,抽象类或普通类均可。
- 默认实现
- 抽象方法不能有实现。
- 虚方法必须实现。
- 子类是否必须实现
- 抽象方法,子类必须实现。
- 虚方法,子类可以不实现。
总之:
- virtual:允许子类重写该方法。基类提供一个默认实现。
- abstract:要求子类必须实现该方法。基类不提供实现。
与Java的区别
- Java只有
override
没有new
,C#有override
和new
。 - C#的
override
只能作用于基类的被virtual
、abstract
或override
修饰的方法;Java不受此限制。 - Java中没有
virtual
。
接口
声明接口
与Java中类似,在C#中,声明接口也是基于interface
关键字,语法格式也是一样的。
1 | public interface 接口名{ |
对于接口名,在C#中,约定俗成,以I
开头。
在Java中,没有这种约定俗成,但是对于接口的实现,有约定俗成,以接口类名+Impl
命名。
接口实现类
与Java的布尔型没有明显区别。
一个类如果实现接口,需要实现接口中的方法,方法名必须与接口中定义的方法名一致。
接口继承
假设,接口1继承接口2,现在一个类来实现接口1,则该类必须同时实现接口1和接口2中的所有成员。
与抽象类比较
与Java的"抽象类和接口的区别"没有明显区别。
命名空间
与Java中Package比较
作用
C#的namespace
和Java的package
都是用来组织代码的一种方式,都有助于避免命名冲突,并且可以将相关的类、接口等类型组织在一起。
相同点
- 组织代码
两者都提供了一种逻辑分组的方式,可以将相关的类和接口放在同一个包或命名空间下。 - 避免名称冲突
通过使用不同的包名或命名空间,即使两个类具有相同的名称,也可以区分它们。 - 访问控制
两者都可以影响类和成员的可见性,例如,在C#中可以通过internal
关键字限制同一命名空间下的访问,在Java中可以通过省略访问修饰符来限制为同一包内访问。
不同点
- 物理结构与逻辑结构
在C#中,namespace
不必严格映射到文件系统的目录结构,虽然通常也是这样做的,但不是必须的。C#项目中的命名空间可以更加灵活地定义。
在Java中,package
声明通常对应于文件系统的目录结构。 - 多命名空间支持
C#允许在一个文件中定义多个namespace
,并且可以在一个文件的不同部分定义同一个命名空间的部分内容。
Java要求每个.java
文件只能有一个顶级package
声明,并且该文件中的所有顶级类都必须属于这个包。 - 默认访问级别
在C#中,没有访问修饰符的类型成员默认是private
,而类型本身默认是internal
,意味着它们仅在同一程序集内可见。
在Java中,默认(即没有显式声明public、private或protected)的类和成员只在同一包内可见。 - 嵌套结构
C#支持直接嵌套命名空间,如namespace Outer { namespace Inner { ... } }
。
Java不支持这种嵌套结构;所有的包都是平级的,尽管你可以创建看起来像是嵌套的包名,比如outer.inner
,但实际上它们是独立的包。 - 别名
C#允许给命名空间或者类型起别名,以简化长名称的引用,例如using alias = Very.Long.Namespace;
。
Java没有直接等价的特性,不过可以使用静态导入来减少某些情况下完全限定名的使用。
定义命名空间
使用namespace
关键字,定义命名空间。
1 | namespace 命名空间名 |
在下文的例子中,我们在一个文件中定义多个namespace
,使用命名空间.类名
的形式访问。
示例代码:
1 | namespace MySpace1 |
运行结果:
1 | 编号:01,姓名:李明 |
引用命名空间
使用using
关键字引用命名空间,即告诉编译器后面的代码中我们需要用到某个命名空间。