avatar


1.基础语法和面向对象

本文对C#的"基础语法"和"面向对象"进行总结。

鉴于C#和Java极其相似,本文会大量的将两者进行比较,对于C#和Java没有明显区别的部分,不会讨论。
关于这些部分,可以参考:

概述

什么是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的关系,就像JavaJDK的关系、JavaScriptNode.js的关系。

  • Java是一门编程语言,JDK是可以运行Java的框架。
  • JavaScript是一门编程语言,Node.js是可以运行JavaScript的框架。
  • C#是一门编程语言,.Net是可以运行C#的框架。

除了C#.Net还支持Visual BasicC++等语言。

环境搭建

Windows

Visual Studio

在Windows系统上,推荐Visual Studio这个IDE。

官网:https://visualstudio.microsoft.com

安装

在安装的时候,对于C#场景,建议选择如下"工作负荷":

工作负荷

修改字体

我个人认为Visual Studio 2022默认的字体很别扭,可以参考如下步骤修改字体。

  1. 在菜单栏上,选择工具 > 选项
  2. 在选项列表中,选择环境 > 字体和颜色

修改字体

可以考虑这个字体:Yahei Consolas Hybrid

创建项目

控制台项目

创建控制台项目,步骤如下:

  1. 创建新项目。
    创建新项目
  2. 选择控制台应用(.NET)
    控制台应用(.NET)
  3. 设置项目名称、存储位置、解决方案名、框架。
    设置

至此,控制台项目,创建完成。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp1
{
internal class Program
{
static void Main(string[] args)
{
Console.WriteLine("测试");
// 不立即退出
Console.ReadKey();
}
}
}

点击顶部的ConsoleApp,启动。

启动

运行结果:

控制台项目

Winform项目

创建一个Winform项目

创建

创建完成后,默认左侧没有"工具箱"的Tab,需要在菜单栏中依次选择视图 > 工具箱,打开工具箱Tab。

Winform项目-工具箱

特别的,在右侧会看到解决方案,中有Form1.csForm1.Designer.cs两个文件。
这两个文件都定义了Form1类。
如果我们点进源码,会发现Form1类使用了Partial关键词声明,被Partial修饰的类可以在多个地方被定义,最后编译的时候会被当作一个类来处理。
在这里,两个文件各司其职,最后合并为一个类编译。

Form1

MacOS

配置

具体可以参考微软官方教程:

https://learn.microsoft.com/zh-cn/dotnet/core/install/macos

关键步骤如下:

  1. 下载并安装.NET SDK
  2. 为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,创建项目。

Create .NET Project

可以点击右上角,运行项目。

Run .NET Project

Linux

说明

在Linux上的步骤,和在MacOS类似,推荐的IDE也是Visual Code。

不过,一般不会在Linux上开发基于C#语言的程序。
但是,常有在Linux上的部署事宜。
本文讨论在Linux上的部署。

安装DotNET

以Rocky这个发行版为例,安装命令如下:

1
dnf install dotnet-sdk-8.0

在有些系统下,可能需要先安装对应的源,步骤如下:

  1. 安装源:
    1
    rpm -Uvh https://packages.microsoft.com/config/centos/8/packages-microsoft-prod.rpm
  2. 安装dotnet-sdk-8.0
    1
    yum install dotnet-sdk-8.0

打包

Windows

在Windows上,基于Visual Studio,可以通过GUI界面进行打包。

右键选择发布:

Windows

选择发布路径等:

Windows

MacOS

在MacOS上,通过dotnet publish,打包发布到本地。

示例代码:

1
dotnet publish

运行结果:

1
2
3
4
Determining projects to restore...
All projects are up-to-date for restore.
ConsoleApp1 -> /Users/kaka/Desktop/cp/ConsoleApp1/bin/Release/net8.0/ConsoleApp1.dll
ConsoleApp1 -> /Users/kaka/Desktop/cp/ConsoleApp1/bin/Release/net8.0/publish/

在有些资料建议通过dotnet build进行打包,根据mavennpm等工具的习惯,也应该是build
但是在dotnet中,build生成的内容在bin/Debug/目录下,而不是bin/Release/。这似乎不符合dotnet的习惯。

通过dotnet build生成的内容在bin/Debug/目录下,而不是bin/Release/。示例代码:

1
dotnet build
运行结果:
1
2
3
4
5
6
7
8
9
Determining projects to restore...
All projects are up-to-date for restore.
ConsoleApp1 -> /Users/kaka/Desktop/cp/ConsoleApp1/bin/Debug/net8.0/ConsoleApp1.dll

Build succeeded.
0 Warning(s)
0 Error(s)

Time Elapsed 00:00:01.28

部署

需要把整个publish文件夹的内容,都上传到Linux服务器上。

例如:

1
2
3
4
5
ConsoleApp1
ConsoleApp1.deps.json
ConsoleApp1.dll
ConsoleApp1.pdb
ConsoleApp1.runtimeconfig.json

然后通过dotnet命令启动。

示例代码:

1
dotnet ConsoleApp1.dll

运行结果:

1
Hello, World!

注意!通常情况下,还需要nohup
关于nohup,可以参考《Linux操作系统使用入门:2.命令》

Remote-SSH

什么是Remote-SSH

本质上,和我们通过向日葵,远程一台电脑,进行开发,没有区别。

  • 代码在远程的主机上。
  • DotNet环境也是远程主机上的。
  • 甚至我们在VSCode上安装的插件,也是为远程主机安装的。

搭建

安装插件Remote - SSHRemote ExplorerRemote - SSH: Editing Configuration Files

修改本机.ssh\config文件,新增如下内容:

1
2
3
4
5
# 192.168.13.165
Host 192.168.13.165
HostName 192.168.13.165
Port 22
User root
  • 使用密码登录远程主机。

后续操作,和我们通过向日葵,远程一台电脑,进行开发,没有区别。
只是我们用了一种更友好的方式,更高级的方式,进行远程开发。

开始

例子

如下,是一段C#代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WindowsFormsApp1
{
internal static class Program
{
/// <summary>
/// 应用程序的主入口点。
/// </summary>
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Form1());
}
}
}

解释说明:

  • usingnamespaceinternalstaticclassvoid等,是关键词。
  • using System;,引入命名空间。
  • Program,类型,一个类包含一个或多个方法、属性、变量等。
  • namespace WindowsFormsApp1,当前程序的命名空间,默认与项目同名。
  • //,注释。
  • 方法名、方法体:
    1
    2
    3
    4
    5
    6
    static void Main()
    {
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);
    Application.Run(new Form1());
    }
    在这里,Main方法是程序的入口。

注释

  • 单行注释,以//开头。
  • 多行注释,以/*开头,以*/结尾,/**/之间的所有内容都属于注释内容。
  • 文档注释,以///开头。

标识符

作用:用来为类、变量、函数或任何其他自定义内容命名。

定义规则:

  • 以字母、下划线或@开头,后面可以跟一系列的字母、数字、下划线、@。
  • 第一个字符不能是数字。
  • 不包含任何嵌入的空格或其他符号。
  • 不能是C#的关键字。
  • 区分大小写。
  • 不能与C#的类库名称相同。

关键字

关键字是编译器预先定义好的一些单词,这些关键字对编译器有特殊的意义,不能用作标识符。

C#中的关键字,分为保留关键字和上下文关键字。

上下文关键字,在代码的上下文中具有特殊的意义,例如getset。一般来说,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#中的数据类型可以分为三类:

  1. 值类型Value types
  2. 引用类型References types
  3. 指针类型Pointer types
    为了类型安全,C#默认不支持指针。除非使用unsafe关键词并开启不安全代码(unsafe code)开发模式。

值类型

什么是值类型

和Java中的基本数据类型没有明显区别,包括内存结构都是相似的,只是名称不同。

分类

  1. 整型
  2. 浮点型
  3. 布尔型
  4. 字符型

还有一类值类型,结构体,这个是Java中没有的,会在下文讨论"类和对象"的时候,进行讨论。

整型

概述

根据存储容量大小,可以分为不同的类型。
根据是否支持负数,可以分为有符号和无符号两种。

例如:

  • 有符号整数:sbyteshortintlong
  • 无符号整数:byteushortuintulong

与Java的区别

在很多资料中,都会说,C#在定义long类型的变量时,需要在结尾加L,形如:

1
long l = 99999999999999L;

其原因和Java在定义long类型变量,需要在结尾加L的原因一样,因为默认的整型是int

在我的实际验证中,不加L也可以,可能编译器层面做了优化。

但:建议加上L,这样更清晰。

在Java中,byte是有符号的,但是在C#中byte是无符号的。

浮点型

在Java中,浮点型有float(单精度)、double(双精度)两种。

而C#中,浮点型有三种:

  1. float,单精度
  2. double,双精度
  3. decimal,高精度,精确数字。

与在Java中一样,默认的浮点类型是double,所以:

  • 定义float类型,以fF结尾。
    1
    float f1 = 1.23f;
  • 定义decimal类型,以mM结尾。
    1
    decimal money = 12.34m;

布尔型

与Java的布尔型没有明显区别。

字符型

与Java的字符型没有明显区别。

引用类型

什么是引用类型

引用类型,与Java中的引用类型几乎一致。
也有栈内存、堆内存的概念。

C#中内置的引用类型包括:

  1. Object,对象
  2. Dynamic,动态
  3. string,字符串

对象类型

与在Java中一样,Object,对象类型,是所有数据类型的最终基类。

动态类型

什么是动态类型

动态类型,Dynamic。
动态类型定义的变量,可以存储任何类型的值。

与对象类型的区别

动态类型,似乎和对象类型一样?我们也可以用对象类型的存储任何类型的值。

示例代码:

1
2
object c = "Hello, World!";
Console.WriteLine(c);

区别在于类型检查方面:

  • 对象类型变量的类型检查是在编译时进行的
  • 动态类型变量的类型检查是在程序运行时进行的。

字符串类型

两种定义方式

字符串类型,string,

有两种定义字符串类型的方式,分别是:

  1. " "
  2. @" ",逐字字符串,会自动进行将转义的字符串。

常用属性

  • 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#中有两种形式的类型转换方式:

  1. 隐式类型转换,也被称为自动类型转换。
  2. 显式类型转换,也被称为强制类型转换。

隐式类型转换

与Java中的隐式类型转换没有明显区别。
都是把一个表示数据范围小的数值或者变量赋值给另一个表示数据范围大的变量。
这种转换方式是自动的,隐式的。

1
2
3
4
5
6
7
8
9
10
short sValue = 123;
int count = sValue;
long bigValue = count;
float fVal = 3.5f;
double dVal = fVal;
// double、float类型不能隐式转换为decimal
// decimal decVal = dVal;
float f2 = count;
double dVal2 = count ;
decimal decVal1 = bigValue;

显式类型转换

什么是显式类型转换

把一个表示数据范围大的数值或者变量赋值给另一个表示数据范围小的变量。
这种转换方式是强制的,显式的,需要明确指出。
且,可能会造成数据丢失。

显式类型转换,有四种方式:

  1. (type)value
  2. type.Parse(【string】)
  3. type.TryParse(【string】, out type 变量)
  4. Convert.ToType(value)

(type)value

在下例中,转换前的值是3.56,转换后的值为3,造成了数据丢失。

示例代码:

1
2
3
double dValue = 3.56;
int intValue = (int)dValue;
Console.WriteLine(intValue);

运行结果:

1
3

type.Parse(string)

将字符串转换为对应的值类型,注意入参只能是字符串,且必须是符合规范的字符串。

字符串"3.56"转成float类型的值,是成功的。示例代码:

1
2
float val = float.Parse("3.56");
Console.WriteLine(val);

运行结果:

1
3.56

但是,字符串"3.56",直接转成int类型的值,会失败。示例代码:

1
2
int val = int.Parse("3.56");
Console.WriteLine(val);

运行结果:

1
2
3
4
Unhandled exception. System.FormatException: The input string '3.56' was not in a correct format.
at System.Number.ThrowFormatException[TChar](ReadOnlySpan`1 value)
at System.Int32.Parse(String s)
at Program.<Main>$(String[] args) in /Users/kaka/Desktop/cs/ConsoleApp/Program.cs:line 1

type.TryParse(string,out type 变量)

type.TryParse(string,out type 变量),返回布尔值,表示转换成功或失败。而接收值,作为out type 变量传入。

示例代码:

1
2
3
4
5
6
string str = "12.89";
int reInt = 0;
bool tryParseRes = int.TryParse(str, out reInt);

Console.WriteLine(reInt);
Console.WriteLine(tryParseRes);

运行结果:

1
2
0
False

Convert.ToType(value)

Convert,C#内置的一种工具类。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
// 转换为byte
object oValue = 120;
byte byteValue = Convert.ToByte(oValue);

// 转换为int
string strValue = "7896";
int intValue= Convert.ToInt32(strValue);

// 将double转换为float
double dValue = 34.89;
float fValue=Convert.ToSingle(dValue);

Convert中内置了一系列的类型转换方法,本文不一一列举。

Convert,是一个典型的工具类。

《基于Java的后端开发入门:3.最常用的Java自带的类》我们讨论过工具类的设计思想:

  1. 构造方法用private修饰,防止外界创建对象。
  2. 成员方法用public static修饰,为了使用类名访问成员方法。

查看Convert的源码,会发现,其成员方法使用public static修饰,但这个类没有构造方法。

因为,Convert类被声明为static类,静态类。
其特点有:

  • 不能被实例化。
    编译器会自动为静态类生成一个私有的构造函数,以实现"不能被实例化"。
  • 不能包含实例构造函数。
  • 只能包含静态成员。

运算符

分类

与Java中一样,常见的运算符也可以分为如下七类:

  1. 算数运算符
  2. 字符串连接运算符
  3. 赋值运算符
  4. 自增自减运算符关系运算符
  5. 逻辑运算符
  6. 短路逻辑运算符
  7. 三元运算符

算术运算符

与Java中的算术运算符没有明显区别。

字符串连接运算符

与Java中的字符串连接运算符没有明显区别。

赋值运算符

与Java中的赋值运算符没有明显区别,也隐含了强制类型转换。

示例代码:

1
2
3
4
5
6
7
8
short s = 10;

// 此行代码报错,因为运算中s提升为int类型,运算结果int赋值给short可能损失精度
// s = s + 10;

// 此行代码没有问题,隐含了强制类型转换,相当于 s = (short) (s + 10);
s += 10;
Console.WriteLine(s);

运行结果:

1
20

自增自减运算符

与Java中的自增自减运算符没有明显区别。

  • 单独使用的时候:
    ++--无论是放在变量的前边还是后边,结果是一样的。
  • 参与操作的时候:
    • 如果放在变量的后边,先拿变量参与操作,后拿变量做++或者--
    • 如果放在变量的前边,先拿变量做++或者--,后拿变量参与操作。

关系运算符

与Java中的关系运算符没有明显区别。

逻辑运算符

与Java中的逻辑运算符没有明显区别。

短路逻辑运算符

与Java中的短路逻辑运算符没有明显区别。

三元运算符

与Java中的三元运算符没有明显区别。

流程控制

条件分支

if结构

与Java中的if结构没有明显区别。

switch结构

去Java的区别

  1. 数据类型支持:
    • Java:switch表达式支持byte, short, char, int、枚举类型和字符串。
    • C#:除了支持类似的数据类型外,还支持long类型。
  2. 模式匹配:
    基于模式匹配,C#中的switch语句可以用于更加复杂的条件判断,而不仅仅是简单的相等比较。

模式匹配

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
object value = "Hello, world!";
switch (value) {
case null:
Console.WriteLine("The value is null.");
break;
case int i when i > 0:
Console.WriteLine($"Positive integer: {i}");
break;
case string s when s.Length > 5:
Console.WriteLine($"Long string: {s}");
break;
case string s:
Console.WriteLine($"Short string: {s}");
break;
case var v:
Console.WriteLine($"Unexpected type: {v.GetType().Name} with value: {v}");
break;
}

运行结果:

1
Long string: Hello, world!

循环分支

for循环

与Java中的for循环没有明显区别。

while循环

与Java中的while循环没有明显区别。

do…while

与Java中的do…while循环没有明显区别。

foreach

foreach,用于遍历数组或者集合对象中的每一个元素。

语法格式:

1
2
3
4
foreach(数据类型 变量名 in 数组或集合对象)
{
循环体;
}

示例代码:

1
2
3
4
5
string[] names = new string[] { "Lily","Lucy","Jason","Steven","White" };
foreach (string name in names)
{
Console.WriteLine("{0} ", name);
}

运行结果:

1
2
3
4
5
Lily 
Lucy
Jason
Steven
White

对应Java中的"增强for循环",语法格式如下:

1
2
3
for(元素数据类型 变量名 : 数组/集合对象名) {
循环体;
}

数组

基本操作

和数组的基本操作相关的重要内容有:

  1. 数组初始化
    1. 静态初始化
    2. 动态初始化
  2. 数组元素访问
  3. 内存结构

这些和Java中数组的基本操作没有明显区别。

多维数组

声明多维数组

1
2
3
4
// 声明一个二维数组
int[,] arr01=new int[3,4];
// 声明一个三维数组
short[,,] arr02=new short[2,3,2];

初始化

指定维度

1
2
3
4
5
int[,] arr = new int[3,2]{
{10,22},
{45,33},
{99,20}
};

不指定维度

1
2
3
4
int[,] arr = new int[,]{
{1,4,9,10,20},
{2,8,11,20,32}
};

直接赋值,不用new

1
2
3
4
5
int[,] arr = {
{0, 11, 12, 23},
{24, 35, 16, 37},
{18, 29, 30, 71}
};

访问

使用arr[i,j]的形式来访问二维数组中的每个元素。

在下例中,我们遍历数组中每一个元素,示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int[,] arr = new int[3,4]
{
{0, 11, 12, 23},
{24, 35, 16, 37},
{18, 29, 30, 71}
};

// 遍历数组
for (int i = 0; i < arr.GetLength(0); i++)
{
for (int j = 0; j < arr.GetLength(1); j++)
{
Console.WriteLine("arr[{0},{1}] = {2}", i, j, arr[i, j]);
}
}
  • GetLength(i)获取指定维中的元素个数。

与Java中多维数组的区别

  1. 语法存在区别。
  2. 在Java中,多维数组实际上是一维数组的一维数组(即数组的数组),因此每一行可以有不同的长度(称为"锯齿数组"或"非矩形数组")。

在下文的Java语言的例子,arr的第二个元素,长度为3,而其他的都是4。这在Java中是被允许的。

1
2
3
4
5
6
7
8
9
10
11
12
int[][] arr = {
{0, 11, 12, 23},
{24, 35, 16},
{18, 29, 30, 71}
};

// 遍历数组
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr[i].length; j++) {
System.out.println("arr[" + i + "][" + j + "] = " + arr[i][j]);
}
}

但是在C#中,这不被允许:

与Java中多维数组的区别

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的方法,没有明显区别,包括其组成部份都是一样的,由以下部分组成:

  1. 访问权限修饰符
  2. 返回值类型
  3. 方法名称
  4. 参数列表
  5. 方法主体

语法格式

与Java中声明方法的语法格式没有明显区别,如下:

1
2
3
4
5
6
7
8
9
访问修饰符 返回值类型 方法名(参数列表)
{

​ 代码语句块

// 如有返回值
return ...;

}

参数传递

与Java的区别

在C#中,有三种参数传递方式:

  1. 值传递(传递基本类型)
  2. 引用传递(传递引用类型)
  3. 输出传递

其中值传递和引用传递,与Java中的没有明显区别。输出传递,是Java所没有的特性。

输出传递

输出传递,其参数用out进行修饰。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
int a = 10;
int b = 20;

int AddOut(out int num1, int num2)
{
num1 = num2 + 2;
Console.WriteLine("内部:a=" + num1);
return num1 + num2;
}

Console.WriteLine("调用前:a=" + a);
int sum = AddOut(out a, b);
Console.WriteLine("调用后:a=" + a);

运行结果:

1
2
3
调用前:a=10
内部:a=22
调用后:a=22

参数数组

参数数组,即参数个数可变,对应Java中的可变参数。

在语法方面存在差异,Java中可变参数的语法格式如下:

1
修饰符 返回值类型 方法名(数据类型... 变量名) {  }

在C#中,格式如下:

1
修饰符 返回值类型 方法名(params 类型名称[] 数组名称) {}

使用参数数组时,调用方法时,既可以直接为方法传递一个数组作为参数,也可以使用函数名(参数1, 参数2, ..., 参数n)的形式传递若干个具体的值。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static void ShowMessage(params string[] infos)
{
int i = 0;
foreach (string info in infos)
{
if (i>0)
Console.Write(",");
Console.Write(info);
i++;
}
Console.WriteLine();
}

ShowMessage(new string[] { "Leah", "Welcome", "the class" });
ShowMessage("Leah", "Welcome", "the class", "Jovan", "Zilor");

运行结果:

1
2
Leah,Welcome,the class
Leah,Welcome,the class,Jovan,Zilor

类和对象

语法格式

与Java中,类的语法格式,没有明显区别。

都是由成员变量和成员方法组成:

1
2
3
4
5
6
7
8
9
10
11
public class 类名 {
// 成员变量
变量1的数据类型 变量1
变量2的数据类型 变量2;
...

// 成员方法
方法1;
方法2;
...
}

创建对象

C#中的对象、类和对象的关系、对象的创建方法,和Java中没有明显区别。

1
类名 对象名 = new 类名();

属性

什么是属性

在Java中,一种我们常见的操作,是将成员变量设置为private,再用定义publicgettersetter方法。

就这种操作规范,在C#中,被称为属性。

属性(Property):类(class)、结构体(structure)和接口(interface)都可以包含,使用访问器(accessors)可以进行读写的,私有的,成员变量。

访问器

什么是访问器

访问器,类似Java中,被public修饰的gettersetter方法。

使用访问器

在C#中,使用访问器,在代码上,更精简。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
namespace ConsoleApp
{
public class Course
{
// 定义CourseId属性
private int id;
public int CourseId
{
get { return id; }
set { id = value; }
}

//CourseName属性 自动属性
public string CourseName { get; set; }

//显示课程信息的方法
public void ShowCourse()
{
Console.WriteLine($"课程编号:{CourseId}");
Console.WriteLine($"课程名称:{CourseName}");
}
}

class Program
{
static void Main(string[] args)
{
// 类中属性的使用:
Course course01 = new Course();

course01.CourseId = 101;
course01.CourseName = "C#开发入门经典(现代生态)";
course01.ShowCourse();
}
}
}

运行结果:

1
2
课程编号:101
课程名称:C#开发入门经典(现代生态)

索引器

什么是索引器

允许在访问器的getset方法上,传入其他参数。

例如,我们可以基于索引器,实现对数组类型更精细的访问。

语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
element-type this[int index]
{
// get 访问器
get
{
// 返回 index 指定的值
}

// set 访问器
set
{
// 设置 index 指定的值
}
}

案例

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
class IndexedNames
{
private string[] namelist = new string[size];
static public int size = 10;
public IndexedNames()
{
for (int i = 0; i < size; i++)
namelist[i] = "N. A.";
}
public string this[int index]
{
get
{
string tmp;

if (index >= 0 && index <= size - 1)
{
tmp = namelist[index];
}
else
{
tmp = "";
}

return (tmp);
}
set
{
if (index >= 0 && index <= size - 1)
{
namelist[index] = value;
}
}
}

static void Main(string[] args)
{
IndexedNames names = new IndexedNames();
names[0] = "Zara";
names[1] = "Riz";
names[2] = "Nuha";
names[3] = "Asif";
names[4] = "Davinder";
names[5] = "Sunil";
names[6] = "Rubic";
for (int i = 0; i < IndexedNames.size; i++)
{
Console.WriteLine(names[i]);
}
Console.ReadKey();
}
}

运行结果:

1
2
3
4
5
6
7
8
9
10
Zara
Riz
Nuha
Asif
Davinder
Sunil
Rubic
N. A.
N. A.
N. A.

重载索引器

索引器可以被重载,没有必要让索引器必须是整型的,可以是其他类型。

在下文的例子中,public string this[int index]public int this[string name],重载了索引器,采用了字符串类型的索引。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
class IndexedNames
{
private string[] namelist = new string[size];
static public int size = 10;
public IndexedNames()
{
for (int i = 0; i < size; i++)
{
namelist[i] = "N. A.";
}
}
public string this[int index]
{
get
{
string tmp;

if (index >= 0 && index <= size - 1)
{
tmp = namelist[index];
}
else
{
tmp = "";
}

return (tmp);
}
set
{
if (index >= 0 && index <= size - 1)
{
namelist[index] = value;
}
}
}
public int this[string name]
{
get
{
int index = 0;
while (index < size)
{
if (namelist[index] == name)
{
return index;
}
index++;
}
return index;
}

}

static void Main(string[] args)
{
IndexedNames names = new IndexedNames();
names[0] = "Zara";
names[1] = "Riz";
names[2] = "Nuha";
names[3] = "Asif";
names[4] = "Davinder";
names[5] = "Sunil";
names[6] = "Rubic";
// 使用带有 int 参数的第一个索引器
// for (int i = 0; i < IndexedNames.size; i++)
// {
// Console.WriteLine(names[i]);
// }
// 使用带有 string 参数的第二个索引器
Console.WriteLine(names["Nuha"]);
Console.ReadKey();
}
}

运行结果:

1
2

抽象类

与Java中的抽象类一样,C#中的抽样类也是使用abstract关键字创建。
其规则也和Java中的抽象类一样:无法直接基于抽象类实例化,必须通过子类对象实例化。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 抽象类
public abstract class People
{
// 抽象属性
public abstract int Age { get; set; }
public abstract void Work();
}

// 学生类 抽象类People的派生类
public class Student : People
{
private int studentId;
private string studentName;
private int age;

public override int Age
{
get { return age; }
set { age = value; }
}

public Student(int id, string name)
{
studentId = id;
studentName = name;
}

public override void Work()
{
Console.WriteLine("编号:" + studentId + ",姓名:" + studentName + ",年龄:" + Age + " 在学校学习!");
}
}

class Program
{
static void Main(string[] args)
{
Student student = new Student(101, "李红");
student.Age = 25;
student.Work();
}
}

运行结果:

1
编号:101,姓名:李红,年龄:25 在学校学习!

解释说明:public abstract int Age { get; set; }是抽象属性,在派生类Student中实现。

静态类

什么是静态类

在上文讨论Convert的时候,我们提到了静态类。

静态类,用static关键字修饰,静态类不能被实例化,静态类中的成员也必须是静态的。

静态类一般用于封装通用处理类,里边封装相关的一系列通用方法,可重用。即,静态类一般都是"工具类"。

特点

  1. 只包含静态成员(静态成员变量、静态成员方法)。
    包括,可以包含静态构造函数。
  2. 无法实例化。
  3. 无法派生子类。
  4. 不能包含实例构造函数。

密封类

密封类,使用sealed关键字修饰,无法被继承。

构造函数

什么是构造函数

与Java中的构造函数一样。
在C#中,构造函数是与类(或结构体)具有相同名称的成员函数,当创建一个类的对象时会自动调用类中的构造函数。通常使用类中的构造函数来初始化类中的成员变量。

与Java中一样,注意:

  • 构造方法的创建方面:
    • 如果没有定义构造方法,系统将给出一个默认的无参数构造方法。
    • 如果定义了构造方法,系统将不再提供默认的构造方法。
  • 构造方法的重载:
    • 如果自定义了带参构造方法,还要使用无参数构造方法,那怎么办呢?构造方法的重载,再写一个无参数构造方法。

在C#中,构造函数可以分为三种:

  1. 实例构造函数
    public修饰的构造函数。
  2. 私有构造函数
    private修饰的构造函数,防止外部类创建该类的实例。
  3. 静态构造函数

静态构造函数

静态构造函数用于初始化类的静态数据或执行一次性操作。它在类的第一个实例创建或静态成员首次被访问时自动调用。

静态构造函数的特点:

  1. 无访问修饰符和参数。
  2. 每个类只能有一个静态构造函数。
  3. 不能继承、重载或直接调用。
  4. 执行时间由CLR控制,且在实例构造函数之前运行。

在下文的例子中,我们会看到先执行静态构造函数,再执行实例构造函数。示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Teacher
{
public static int count=0;
public Teacher()
{
count=1;
}
static Teacher()
{
count=3;
}
}

class Program
{
static void Main(string[] args)
{
Console.WriteLine($"count = {Teacher.count}");
Teacher teacher1 = new Teacher();
Console.WriteLine($"count = {Teacher.count}");
}
}

运行结果:

1
2
count = 3
count = 1

结构体

什么是结构体

结构体,Struct,一种值类型(value type),用于组织和存储相关数据。

定义结构体

定义结构体,需要基于struct关键词。

1
2
3
4
5
6
7
struct Books
{
public string title;
public string author;
public string subject;
public int book_id;
};

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
struct Books
{
public string title;
public string author;
public string subject;
public int book_id;
};

public class TestStructure
{
public static void Main(string[] args)
{

Books Book1;
Books Book2;

Book1.title = "C Programming";
Book1.author = "Nuha Ali";
Book1.subject = "C Programming Tutorial";
Book1.book_id = 6495407;

Book2.title = "Telecom Billing";
Book2.author = "Zara Ali";
Book2.subject = "Telecom Billing Tutorial";
Book2.book_id = 6495700;

Console.WriteLine( "Book 1 title : {0}", Book1.title);
Console.WriteLine("Book 1 author : {0}", Book1.author);
Console.WriteLine("Book 1 subject : {0}", Book1.subject);
Console.WriteLine("Book 1 book_id :{0}", Book1.book_id);

Console.WriteLine("Book 2 title : {0}", Book2.title);
Console.WriteLine("Book 2 author : {0}", Book2.author);
Console.WriteLine("Book 2 subject : {0}", Book2.subject);
Console.WriteLine("Book 2 book_id : {0}", Book2.book_id);

Console.ReadKey();

}
}

运行结果:

1
2
3
4
5
6
7
8
Book 1 title : C Programming
Book 1 author : Nuha Ali
Book 1 subject : C Programming Tutorial
Book 1 book_id :6495407
Book 2 title : Telecom Billing
Book 2 author : Zara Ali
Book 2 subject : Telecom Billing Tutorial
Book 2 book_id : 6495700

和类的区别

  1. 数据类型
    结构是值类型(Value Type)
    类是引用类型(Reference Type)
  2. 继承
    结构不能继承其他结构或类,也不能作为其他结构或类的基类。
    类支持继承。
  3. 默认构造函数:
    结构不能有无参数的构造函数。
    类可以有无参数的构造函数。
  4. 赋值行为
    结构变量在赋值时会复制整个结构,因此每个变量都有自己的独立副本。
    类型为类的变量在赋值时存储的是引用,因此两个变量指向同一个对象。
  5. 传递方式:
    结构对象通常通过值传递。
    类型为类的对象在方法调用时通过引用传递。
  6. 可空性:
    结构体是值类型,不能直接设置为null(可以使用Nullable<T>或称为T?的可空类型)。
    类的实例默认可以为null
  7. 性能和内存分配:
    结构通常更轻量,在栈上分配内存,适用于简单的数据表示。
    类涉及更多的内存开销和管理。

枚举

和Java中的枚举没有明显区别。

继承

什么是继承

和Java中的继承没有明显区别。

同样,只支持单继承,一个派生类只能继承一个基类;不支持多重继承,即不支持一个类同时继承多个基类;继承可以传递。

语法格式

在Java中,继承基于extends关键字;在C#中,是:

1
2
3
4
class 派生类:基类
{

}

多态

分类

  1. 静态多态,编译时多态
    通过"方法重载"和"运算符重载"等实现编译时多态。
  2. 动态多态,运行时多态
    通过方法重写实现的运行时多态。

静态多态

方法重载

在同一个作用域中,可以定义多个同名的方法,但是这些方法彼此之间必须有所差异,比如参数个数不同或参数类型不同等等,返回值类可以不同。

运算符重载

运算符重载基于operator关键字后跟运算符的形式来定义的,我们可以将被重新定义的运算符看作是具有特殊名称的函数,与其他函数一样,该函数也有返回值类型和参数列表。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class StringNew
{
public string Str1 { get; set; }
public string Str2 { get; set; }

public void SetStrs(string str1, string str2)
{
Str1 = str1;
Str2 = str2;
}

public static StringNew operator +(StringNew s1, StringNew s2)
{
StringNew str = new StringNew();
str.Str1 = (int.Parse(s1.Str1) + int.Parse(s2.Str1)).ToString();
str.Str2 = (int.Parse(s1.Str2) + int.Parse(s2.Str2)).ToString();
return str;
}
}

class Program
{
static void Main(string[] args)
{
StringNew str1 = new StringNew();
StringNew str2 = new StringNew();
str1.SetStrs("12", "23");
str2.SetStrs("30", "40");
StringNew str3 = new StringNew();
str3 = str1 + str2;
Console.WriteLine(str3.Str1 + "," + str3.Str2); //42,63
}
}

运行结果:

1
42,63

动态多态

override和new

在C#中,子类"重写"父类的方法,有两种方式:

  1. override
  2. new

而在Java中,有且仅有一种方式,override

两种方式的区别在于:

  1. override、直译"推翻"、即子类直接推翻父类的方法,也就是所谓的"覆盖"、“重写”、“覆写”。
  2. new、直译"新建",子类新建一个方法,子类和父类的方法同时存在。

具体可以看下面的例子。
Derived继承了Base,以override的方式重写了Method1,以new的方式新建了Method2;当Base baseClass= new Derived();时,发生了一次向上转型,baseClassDerived转型为其父类Base类型;baseClass.Method1调用的是被override之后的Derived.Method1baseClass.Method2调用的是Base.Method2,因为baseClass.Method2没有被影响到。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class Base
{
public virtual void Method1()
{
Console.WriteLine("Base's virtual function Method1");
}

public virtual void Method2()
{
Console.WriteLine("Base's virtual fucntion Method2");
}
}
public class Derived : Base
{
public override void Method1()
{
Console.WriteLine("Derived's override function Method1");
}

public new void Method2()
{
Console.WriteLine("Derived's new function Method2");
}
}

public class Program
{
public static void Main(string[] args)
{
Base baseClass = new Derived();
baseClass.Method1();
baseClass.Method2();
}
}

运行结果:

1
2
Derived's override function Method1
Base's virtual fucntion Method2

虚方法和抽象方法

虚方法

virtual修饰的方法。

对于虚方法,子类可以将其override,也可以将其new

抽象方法

abstract修饰的方法。

特点:

  1. 只能在抽象类中定义,由抽象类的派生类中实现。
  2. 子类只能override抽象方法,不能new抽象方法。
  3. 子类必须实现抽象方法。

区别

  1. 关键字
    • 抽象方法:abstract
    • 虚方法:virtual
  2. 定义位置
    • 抽象方法,必须在抽象类中。
    • 虚方法,抽象类或普通类均可。
  3. 默认实现
    • 抽象方法不能有实现。
    • 虚方法必须实现。
  4. 子类是否必须实现
    • 抽象方法,子类必须实现。
    • 虚方法,子类可以不实现。

总之:

  • virtual:允许子类重写该方法。基类提供一个默认实现。
  • abstract:要求子类必须实现该方法。基类不提供实现。

与Java的区别

  1. Java只有override没有new,C#有overridenew
  2. C#的override只能作用于基类的被virtualabstractoverride修饰的方法;Java不受此限制。
  3. Java中没有virtual

接口

声明接口

与Java中类似,在C#中,声明接口也是基于interface关键字,语法格式也是一样的。

1
2
3
4
5
public interface 接口名{
返回值类型 方法名1(参数列表...);
返回值类型 方法名2(参数列表...);
... ...
}

对于接口名,在C#中,约定俗成,以I开头。

在Java中,没有这种约定俗成,但是对于接口的实现,有约定俗成,以接口类名+Impl命名。

接口实现类

与Java的布尔型没有明显区别。

一个类如果实现接口,需要实现接口中的方法,方法名必须与接口中定义的方法名一致。

接口继承

假设,接口1继承接口2,现在一个类来实现接口1,则该类必须同时实现接口1和接口2中的所有成员。

与抽象类比较

与Java的"抽象类和接口的区别"没有明显区别。

命名空间

与Java中Package比较

作用

C#的namespace和Java的package都是用来组织代码的一种方式,都有助于避免命名冲突,并且可以将相关的类、接口等类型组织在一起。

相同点

  1. 组织代码
    两者都提供了一种逻辑分组的方式,可以将相关的类和接口放在同一个包或命名空间下。
  2. 避免名称冲突
    通过使用不同的包名或命名空间,即使两个类具有相同的名称,也可以区分它们。
  3. 访问控制
    两者都可以影响类和成员的可见性,例如,在C#中可以通过internal关键字限制同一命名空间下的访问,在Java中可以通过省略访问修饰符来限制为同一包内访问。

不同点

  1. 物理结构与逻辑结构
    在C#中,namespace不必严格映射到文件系统的目录结构,虽然通常也是这样做的,但不是必须的。C#项目中的命名空间可以更加灵活地定义。
    在Java中,package声明通常对应于文件系统的目录结构。
  2. 多命名空间支持
    C#允许在一个文件中定义多个namespace,并且可以在一个文件的不同部分定义同一个命名空间的部分内容。
    Java要求每个.java文件只能有一个顶级package声明,并且该文件中的所有顶级类都必须属于这个包。
  3. 默认访问级别
    在C#中,没有访问修饰符的类型成员默认是private,而类型本身默认是internal,意味着它们仅在同一程序集内可见。
    在Java中,默认(即没有显式声明public、private或protected)的类和成员只在同一包内可见。
  4. 嵌套结构
    C#支持直接嵌套命名空间,如namespace Outer { namespace Inner { ... } }
    Java不支持这种嵌套结构;所有的包都是平级的,尽管你可以创建看起来像是嵌套的包名,比如outer.inner,但实际上它们是独立的包。
  5. 别名
    C#允许给命名空间或者类型起别名,以简化长名称的引用,例如using alias = Very.Long.Namespace;
    Java没有直接等价的特性,不过可以使用静态导入来减少某些情况下完全限定名的使用。

定义命名空间

使用namespace关键字,定义命名空间。

1
2
3
4
namespace 命名空间名
{
// 命名空间中的代码 类及类中的成员
}

在下文的例子中,我们在一个文件中定义多个namespace,使用命名空间.类名的形式访问。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
namespace MySpace1
{
public class UserInfo
{
public void Show()
{
Console.WriteLine("编号:01,姓名:李明");
}
}
}

namespace MySpace2
{
public class UserInfo
{
public void Show()
{
Console.WriteLine("编号:02,姓名:王林");
}
}
}

namespace kaka.ConsoleApp
{
class Program
{
static void Main(string[] args)
{
MySpace1.UserInfo user01 = new MySpace1.UserInfo();
user01.Show();
MySpace2.UserInfo user02 = new MySpace2.UserInfo();
user02.Show();
}
}
}

运行结果:

1
2
编号:01,姓名:李明
编号:02,姓名:王林

引用命名空间

使用using关键字引用命名空间,即告诉编译器后面的代码中我们需要用到某个命名空间。

文章作者: Kaka Wan Yifan
文章链接: https://kakawanyifan.com/11801
版权声明: 本博客所有文章版权为文章作者所有,未经书面许可,任何机构和个人不得以任何形式转载、摘编或复制。

留言板