Csharp

前言

c语言并非面向对象而后出现面向对象的编程语言c++ 但存在很多复杂的成分

java的出现被誉为c++--,去除了一些c++中复杂的部分

而后微软为了对抗java研发了c#语言

c#应该称得上是继c++、java后的第三大面对对象的编程语言

相较于两者 它的特征最丰富、也是用起来也最方便的一种语言

它基于.net运行环境

.NET运行环境是微软公司开发的一个运行时环境,用于支持.NET Framework应用程序的执行。它包括两个主要组件:Common Language Runtime (CLR) 和 Base Class Library (BCL)。CLR是.NET运行环境的核心组件,它用于解释并执行.NET应用程序代码,提供了许多关键功能,例如内存管理、类型安全、异常处理等。BCL是一组可重用的类库,为.NET应用程序提供了许多常见的功能,例如字符串处理、网络通信、文件和I/O等。.NET运行环境还提供了一些工具和技术,用于简化.NET应用程序的开发和部署,例如Visual Studio

因为我觉得c++、c#都是基于c语言的一种封装、改进

所以这份笔记会多从c的角度去思考

其实本来是基于c++的 但由于前段时间偷懒 没能先学完c++所以只能基于c了

将围绕三点:

c#对于c做了哪些改进?

我能理解这些改进的高明之处吗?

我是否也能用c语言做出类似的改进呢?

命名空间

作为一个非常严格的面对对象的语言

即使是在hello world这样的简单程序中都能体现出它的特点

我们可以无需头文件(include)、命名空间(namespace)

直接使用System.Console.WriteLine("Hello, World!");这样一行代码就能执行成功

csharp-image1

如何理解?

C#使用了命名空间的概念,命名空间可以帮助组织和管理类和其他类型

类似于文件夹可以帮助组织和管理文件,把类想象成文件,使用它们的时候就像打开文件一样

使用类的时候,可以直接写出类的全名,就像写出文件的完整路径一样,不需要额外引用头文件

在C#中,System是一个包含许多常用类型的命名空间,而Console是其中一个类,它提供了在控制台中输出信息的功能

因此,通过使用类的全限定名,可以直接访问该类的成员,而不需要在程序中使用头文件来引入该类,就像刚才那样输出“Hello, World!”,可以使用System.Console.WriteLine("Hello, World!"); 或者将命名空间写在前面 这样就能省略system.

再分析一下命名空间和头文件的区别

首先他们都是用于组织和管理代码的工具

区别在于它们的具体实现方式和作用不同

头文件实际上做的是文本替换,将函数、变量和结构体等声明和定义封装到一个文件中,程序需要使用其中的内容时,只需要在程序中包含对应的头文件引用和使用即可(在程序中引入外部代码)。像在拼图中使用一个个独立的零件,将各种类型的声明和定义封装到一个文件中,在需要使用它们的地方进行引用。

命名空间是用来组织和管理代码的一种机制,将类、接口、枚举和委托等类型封装到一个区域中,以便在程序中引用和使用。通过使用命名空间,可以避免不同模块之间的命名冲突,不同的命名空间中可以使用相同的名称。(解决可能存在的重复声明问题)另外,命名空间还可以帮助组织和管理代码,使代码结构更加清晰。就像是在组织和分类拼图零件,不同的命名空间如同不同的盒子,将具有相似功能或者属于同一类型的零件归纳在一起,这样一方面便于使用,另一方面也便于管理和维护。

其实这里也能看出 命名空间也带有一番类的味道

将相关的代码封装在一起,通过命名空间来组织和管理代码,从而使代码结构更加清晰,可读性更高。

来看一段chatgpt做出的解释:

当我们写程序时,为了避免代码的冲突和混乱,我们需要将变量、函数、类等代码块封装起来,以便在需要的时候调用。在C语言中,我们通常使用头文件来实现这种封装,将不同的代码块放在不同的头文件中,通过#include指令将其引入到程序中使用。

而在C#中,我们使用命名空间来实现这种封装。命名空间就像是一个大的容器,里面包含着一组相关的代码块。不同的命名空间就像是不同的容器,将不同的代码块进行分类、组织和管理。这样可以使程序结构更加清晰,代码更加可读和易于维护。

举个例子,假如我们要写一个程序,其中包含了很多不同类型的动物,比如狗、猫、鸟等。如果使用C语言的方式,我们需要为每种动物都定义一个头文件,比如dog.h、cat.h、bird.h等,然后在程序中通过#include指令引入相应的头文件。这样会使代码变得非常冗长和繁琐。

而在C#中,我们可以使用命名空间来将这些代码块进行封装。我们可以为所有的动物定义一个Animals命名空间,然后将Dog、Cat、Bird等类放在这个命名空间中。这样就可以通过Animals.Dog、Animals.Cat、Animals.Bird等方式来引用相应的代码块,代码看起来更加简洁明了。

总之,命名空间是C#中的一个非常重要的概念,它可以帮助我们组织和管理代码,使程序结构更加清晰,代码更加可读和易于维护。

https://chatdoc.com/(chatgpt文档检查)
csharp-image2 csharp-image3

为了编程方便 把所有命名空间都给引用进去 那写什么都能被识别了???

并不是这样

以一个很形象的比喻

就像是图书馆 不同种类的书放在不同的模块区域

这些不同模块的书其实会有很多重名的

而在同一个模块里面 就能大幅减少这种情况

再比如文件资源管理器 同一文件夹里不能有同名文件 但不同文件夹里面可以有同名文件

Namespace起到的作用有这点——防止重名导致混淆

main入口

根据之前的认知 我们知道 程序是从main函数开始执行的

可在上述的代码中 我甚至没给出任何东西 就是单纯一行语句就能执行

经过询问chatgpt我了解到

在 C# 中,如果没有显式地定义 Main 方法,编译器会自动为你生成一个默认的 Main 方法,并将第一条语句作为程序的入口点。而在 C 语言中,你必须显式地定义一个名为 main 的函数,并将其作为程序的入口点。

但这种省略main的写法不推荐使用,因为它会让代码变得难以理解和维护。

关于c#中对于main的写法 有三种

(1)使用 Main 方法作为程序入口点。这是最常见的写法,也是最推荐的写法。

static void Main(string[] args)
{
    // 程序代码
}

(2)使用类中的静态方法作为程序入口点。在这种情况下,编译器会自动将静态方法识别为程序的入口点。

static class Program
{
    static void Main()
    {
        // 程序代码
    }
}

(3)使用命名空间中的静态类作为程序入口点。在这种情况下,编译器会自动将静态类中的 Main 方法识别为程序的入口点。

namespace MyNamespace
{
    static class Program
    {
        static void Main()
        {
            // 程序代码
        }
    }
}

基础部分与c语言大部分相似 所以仅记载一些多出来的部分

变量定义:

var

可用var关键字自动推断变量类型。

使用var关键字声明变量时,编译器会自动推断变量的类型。这样做的好处是可以减少代码的重复和提高代码的可读性,因为不需要在每个变量声明时都指定类型,而是由编译器根据变量初始化的值来推断出变量的类型。

例如:

var number = 42;//定义了一个整型变量number,其值为42

需要注意的是,使用var关键字声明变量时,变量必须在声明时进行初始化。否则,编译器无法推断出变量的类型。

var name; // 错误,必须进行初始化

另外,使用var关键字声明的变量仍然是具有类型的,只是类型由编译器推断而来。因此,不能将一个var类型的变量赋值给另一个类型不同的变量。

var name = "John";
int age = name; // 错误,不能将string类型赋值给int类型

string

还有值得一提的就是string

这个c++中也有

有了string字符串类型 我们对于一些操作都变的非常方便

最常用的就是可以直接用+将字符串拼接起来

dynamic

dynamic 是 C# 中的一种类型,它是在编译时确定类型的语言中实现动态类型的方式。使用 dynamic 声明的变量在编译时不确定其类型,而是在运行时根据实际情况来确定类型。dynamic 可以表示任何类型,而不需要进行类型转换或 boxing。使用 dynamic 可以简化代码,避免繁琐的类型转换,提高代码的可读性和可维护性。但是,由于 dynamic 是在运行时确定类型的,因此可能会导致运行时类型错误,需要注意使用时的安全性。

dynamic obj = GetDynamicObject();
obj.Method();
obj.Property = value;

上面的obj 变量是一个 dynamic 类型的对象,可以调用 Method() 方法和设置 Property 属性,这些方法和属性只有在运行时才能确定。

var 和 dynamic 都是 C# 中的关键字,它们都可以用来声明变量,但是它们的使用方式和效果是不同的。

var:var 可以用来在编译时自动推断变量类型。也就是说,编译器会根据变量的初始化表达式自动确定变量的类型。例如:var x = 42;,编译器会自动将 x 的类型推断为 int。

需要注意的是,var 声明的变量一旦初始化后,其类型就确定了,不能再次修改

dynamic:dynamic 声明的变量在编译时不会进行类型检查,而是在运行时才会确定其类型。也就是说,dynamic 变量的类型是在运行时动态确定的。由于 dynamic 变量的类型是在运行时确定的,因此可以使用动态绑定的方式调用方法和属性。

例如:dynamic x = 42;
x = "Hello, World!";

在第一行中,x 的类型被动态确定为 int;

在第二行中,x 的类型被动态修改为 string

需要注意的是,使用 dynamic 声明的变量需要谨慎使用,因为编译器无法对其进行类型检查,可能会导致运行时出错。通常情况下,应该尽量避免使用 dynamic,除非必须要使用动态类型才能完成某些操作。而 var 则是一个更加常用和安全的关键字,可以简化代码并提高代码的可读性。

条件语句:

在switch上有些变化

变得更加灵活,如下:

string color = "red";
switch (color)
{
    case "red":
    break;
    case "green":
    break;
    ……
}

循环语句:

foreach语句

可以循环访问数组、集合或其他

例如:

int[] numbers = { 1, 2, 3, 4, 5 };
foreach (int number in numbers)
{
    ……
}

用于循环遍历数组numbers中的元素,并将每个元素输出到控制台

(foreach是只读的)

函数定义:

C#中的函数定义与C语言有些不同,需要使用关键字public或private来指定函数的访问级别。

同时,C#支持可选参数和命名参数。

例如:

public void PrintMessage(string message, int times = 1)
{
    for (int i = 0; i < times; i++)
    {
        Console.WriteLine(message);
    }
}

输入输出

在输入输出方面的变得更加方便,并避免些可能出现的错误。

输入

通过 Console.ReadLine() 方法从控制台读取用户输入的一行字符串

Console.ReadKey() 方法

方便地读取用户输入的一个按键,并返回按键的信息

Console.ReadLine() 方法

读取控制台输入的字符串后,需要对输入的字符串进行类型转换才能得到其他数据类型的值(需要配套类型转换使用)

一些常见的类型转换方法:

int.Parse(string s):将字符串转换为整数。

double.Parse(string s):将字符串转换为双精度浮点数。

bool.Parse(string s):将字符串转换为布尔值。

Convert.ToInt32(string s):将字符串转换为整数。

Convert.ToDouble(string s):将字符串转换为双精度浮点数。

Convert.ToBoolean(string s):将字符串转换为布尔值。

如果输入的字符串不能转换为目标类型,将会抛出异常,因此在进行类型转换时,需要做好异常处理。例如可以使用 try-catch 语句来捕捉异常,然后进行错误提示或者重新要求用户输入。

try
{
    Console.WriteLine("请输入一个整数:");
    string input = Console.ReadLine();
    int num = int.Parse(input); // 尝试将输入的字符串转换为整数
    Console.WriteLine($"您输入的是整数 {num}");
}
catch (FormatException ex)
{

Console.WriteLine("输入格式错误,请重新输入整数。"); // 处理异常情况

}
catch (OverflowException)
{
    Console.WriteLine("输入的数值超出了整数的取值范围!");
}

try代码块中包含可能出现异常的代码 如果在执行 try 代码块时发生了异常 那么控制权将转移到 catch 代码块中 并执行 catch 代码块中的代码来处理异常

在 catch 代码块中,可以使用异常对象 ex 来获取异常信息,并进行处理

通过 int.Parse() 方法尝试将输入的字符串转换为整数,如果转换失败,则会抛出 FormatException 异常;

如果输入的数值超出了整数的取值范围,将会引发 OverflowException 异常。使用 try-catch 语句来捕捉这个异常,并在 catch 代码块中输出错误提示信息。

几种常见写法

使用 Convert 类进行类型转换:

Console.Write("请输入一个整数:");
string input = Console.ReadLine();
int num = Convert.ToInt32(input);
Console.WriteLine($"您输入的是整数 {num}");

使用 int.Parse() 进行类型转换:

Console.Write("请输入一个整数:");
string input = Console.ReadLine();
int num = int.Parse(input);
Console.WriteLine($"您输入的是整数 {num}");

使用 TryParse() 进行类型转换,并判断输入是否合法

Console.Write("请输入一个整数:");
string input = Console.ReadLine();
if (int.TryParse(input, out int num))
{
    Console.WriteLine($"您输入的是整数 {num}");
}
else
{
    Console.WriteLine("输入的内容不是整数!");
}

使用 int.TryParse() 方法来进行类型转换,如果转换成功,将会将转换后的整数值存储在 num 变量中,并执行相应的输出操作;如果转换失败,将会输出相应的错误信息。

try-catch 和 TryParse()

需要注意的是,使用 try-catch 和 TryParse() 的选择取决于具体的情况。

如果只是简单地将用户输入转换为特定的数据类型,并且不需要对转换失败进行详细的处理,那么使用 TryParse() 可能更为简便;如果需要对转换失败进行详细的处理,那么使用 try-catch 可能更为灵活。

以上三种方法都可以实现从控制台读取用户输入并进行类型转换

其中 TryParse() 方法还可以判断输入是否合法,是比较常用的方式。

int num = Convert.ToInt32(Console.ReadLine());

这是我当时自己试出的写法 其实也是可以的 用现在的理解解释一下

readline读入的是一行字符串 显然得用string去接,然后再进行类型转换

当然 像我这样一读进来就转换好也不是不行

但这样就不能进行转换不成功的异常处理了 也是当时找不到出问题的源头所在

输出

在 C# 中,通过 Console.Write() 或 Console.WriteLine() 方法可以方便地输出数据到控制台,并且可以自动格式化输出的内容。

另外,C# 中还提供了许多特殊的输出格式,如 $ 字符串、字符串插值等等,可以更加方便地输出数据,并且避免了在 C 中可能出现的格式化字符串错误和类型错误等问题。

Console.Write() 方法
int num1 = 10;
int num2 = 20;
Console.Write("num1 = ");
Console.Write(num1);
Console.Write(", num2 = ");
Console.Write(num2);
csharp-image4
Console.WriteLine() 方法
string name = "Tom";
int age = 20;
Console.WriteLine("My name is " + name + ", and I'm " + age + " years old.");
csharp-image5

字符串连接符号(+)

int a = 10;
int b = 20;
Console.WriteLine("a = " + a + ", b = " + b + ", a + b = " + (a + b));
csharp-image6

字符串填充({0})

string name = "Tom";
int age = 20;
Console.WriteLine("My name is {0}, and I'm {1} years old.", name, age);

相对于 C 语言中的 printf 函数来说更加灵活和易用。

在 C 语言中,需要在格式字符串中使用特定的占位符来指定要输出的变量的类型和位置,而在 C# 中,我们可以通过占位符的位置或者名称来指定要输出的变量,这使得格式化字符串的使用更加方便和灵活

字符串格式化

其实也属于字符串填充

string name = "张三";
string sex = "男";
string str = string.Format("我的名字是{0},性别是{1}", name, sex);
Console.WriteLine(str);

使用 string.Format() 的主要优势在于可以对输出的格式进行更加灵活的控制,例如控制输出的精度、填充方式等等

double pi = Math.PI;
Console.WriteLine("pi 的值为 {0}", pi);  // 输出 "pi 的值为 3.14159265358979"
Console.WriteLine("pi 的值为 {0:F2}", pi);  // 输出 "pi 的值为 3.14"

{0:F2} 的意思是输出第一个参数(即 pi),并保留两位小数。

如果使用 Console.WriteLine() 的话,则需要先将 pi 转换成字符串并控制其精度,再与文本进行拼接输出

上例不能很好体现出string.Format()拼接好再输出 相较于 直接输出的优点

当需要输出的内容比较复杂或需要进行格式化时,就能更好显示出它的优势

比如要输出这样一个表格

csharp-image7
string name = "Tom";
int age = 18;
double score = 89.5;
string format = "{0,-10}{1,-10}{2,-10}\n{3,-10}{4,-10}{5,-10:F2}";
string table = string.Format(format, "姓名", "年龄", "成绩", "----", "----", "----");
Console.WriteLine(table);
Console.WriteLine(string.Format(format, name, age, score));

使用了这么多次拼接才复杂地得以实现 更别提直接使用console.writeline了

字符串插值($)

string name = "Tom";
int age = 20;
Console.WriteLine($"My name is {name}, and I'm {age} years old.");

对于上面那个例子

csharp-image7

其实可以直接这样写

string name = "Tom";
int age = 18;
double score = 89.5;
Console.WriteLine($"{"姓名",-10}{"年龄",-10}{"成绩",-10}");
Console.WriteLine($"{"----",-10}{"----",-10}{"----",-10}");
Console.WriteLine($"{name,-10}{age,-10}{score,-10:F2}");

肉眼可见的方便了很多

同时也能得知

$ 字符串有占位符和对齐功能,可以很方便地输出表格,而不需要手动计算每个元素所需要的空格数

从连接字符串到字符串插值再到字符串填充和字符串格式化,再到字符串插值,每种方式都相对于之前更加简洁、方便、易读,逐渐优化了字符串输出的体验。

字符串“函数”

在 C# 中,字符串是一种内置类型,而不是像 C 一样是由字符数组表示的数据类型。因此,C# 中的字符串具有许多内置函数和属性,使其易于操作和使用。这些函数和属性包括字符串连接运算符 (+) 和 Length 属性,可以执行类似于 C 中字符串函数的操作,如拼接字符串和获取字符串长度。

虽然 C# 中的字符串函数不需要像 C 中的字符串函数那样使用指针和字符数组,但它们仍然执行着相似的任务,并且通常被认为是字符串函数。

因此,即使在 C# 中使用 + 和 Length等来实现字符串操作,这些操作仍然可以被视为字符串函数的功能。

长度

C中:strlen():

在 C# 中,可以使用 Length 属性来获取字符串的长度。

例如:

string str = "Hello, world!";
int len = str.Length; // 获取字符串的长度,结果为 13

大小写

在 C# 中,可以使用 ToLower 和 ToUpper 方法来进行字符串的大小写转换。

ToLower 方法将字符串中的所有字符转换为小写,

ToUpper 方法将字符串中的所有字符转换为大写。

string str = "Hello World";
string lowerStr = str.ToLower(); // 将字符串转换为小写
Console.WriteLine("Lowercase string: " + lowerStr);
string upperStr = str.ToUpper(); // 将字符串转换为大写
Console.WriteLine("Uppercase string: " + upperStr);

复制

在 C 中:strcpy() 和 strncpy():

在 C# 中,可以使用赋值运算符 =

或者 CopyTo 方法来实现复制字符串的操作。

例如:

string str1 = "Hello, world!";
string str2 = str1; // 使用赋值运算符实现复制操作string str3 = string.Empty;
str1.CopyTo(0, str3, 0, str1.Length); // 使用 CopyTo 方法实现复制操作

提取部分

如果需要复制字符串的一部分,则可以使用 Substring 方法来提取所需的部分

string str1 = "Hello, world!";
string str2 = str1.Substring(7, 5); // 提取 "world"

第七个位置开始 五个长度单位

连接

在 C 中:strcat() 和 strncat():

在 C# 中,可以使用 + 运算符来实现字符串连接的操作。

string str1 = "Hello, ";
string str2 = "world!";
string str3 = str1 + str2; // 字符串连接

如果需要连接的字符串数量较多,则可以使用 String.Concat 方法。

string str1 = "Hello, ";
string str2 = "world";
string str3 = "!";
string str4 = String.Concat(str1, str2, str3); // 字符串连接

比较

在 C 中:strcmp() 和 strncmp():

在 C# 中,可以使用 Equals 方法和 CompareTo 方法来比较字符串。

Equals 方法是区分大小写的,它可以用于比较两个字符串是否相等,如果两个字符串的大小写不一致,则不会被认为是相等的

CompareTo 方法将两个字符串按照字典顺序进行比较。

如果第一个字符串在字典中排在第二个字符串之前,则返回一个负整数;

如果第一个字符串在字典中排在第二个字符串之后,则返回一个正整数;

如果两个字符串相等,则返回 0。

CompareTo 方法也是区分大小写的,如果需要不区分大小写的比较,

可以使用String.Compare方法,并指定 StringComparison.OrdinalIgnoreCase 选项。

Equals
string str1 = "hello";
string str2 = "Hello";
if (str1.Equals(str2))
Console.WriteLine("The two strings are equal");
csharp-image8
else
Console.WriteLine("The two strings are not equal");
csharp-image8
CompareTo
string str1 = "apple";
string str2 = "banana";
int result = str1.CompareTo(str2); // 比较字符串的大小,结果为 -1

如果需要忽略大小写,则可以使用 Compare 方法

Compare

指定 StringComparison.OrdinalIgnoreCase 作为比较选项。

string str1 = "Apple";
string str2 = "aPPle";
int result = String.Compare(str1, str2,StringComparison.OrdinalIgnoreCase);//结果为 0

位置(字符)

在 C 中:strchr() 和 strrchr():

在 C# 中,可以使用 IndexOf 和 LastIndexOf 方法来查找字符在字符串中出现的位置。

例如:

string str = "Hello, world!";
int index1 = str.IndexOf('o'); // 查找第一个 "o" 出现的位置,结果为 4
int index2 = str.LastIndexOf('o'); // 查找最后一个 "o" 出现的位置,结果为 8

位置(字符串)

在 C 中:strstr():

在 C# 中,可以使用 IndexOf 方法来查找子字符串在字符串中出现的位置。

例如:

string str = "Hello, world!";
int index = str.IndexOf("world"); // 查找 "world" 出现的位置,结果为 7

包含

c#中使用 Contains 方法来检查一个字符串是否包含另一个字符串。

如果包含,则返回 true,否则返回 false

Contains 方法是区分大小写的,也就是说,如果被查找的子字符串的大小写和目标字符串不一致,则不会匹配。

string str = "The quick brown fox jumps over the lazy dog.";
if (str.Contains("fox"))
Console.WriteLine("The string contains 'fox'");
else
Console.WriteLine("The string does not contain 'fox'");
if (str.Contains("FOX"))
Console.WriteLine("The string contains 'FOX'");
else
Console.WriteLine("The string does not contain 'FOX'");
csharp-image9

Contains 方法返回的是一个布尔值,因此可以直接用于条件判断。

另外,Contains 方法还有一个重载版本,可以指定字符串比较的方式,包括大小写不敏感和区分区域性等选项。

拆分

在 C 中:strtok():

strtok 函数可以用来将一个字符串按照指定的分隔符拆分成多个字符串。这个函数会从字符串中依次查找分隔符,并将分隔符前后的部分作为一个字符串返回。每次调用 strtok 函数时,它会从上一次查找的位置开始向后查找,直到找到下一个分隔符为止。当所有分隔符都被找到后,strtok 函数返回null。

在 C# 中,可以使用 Split 方法来将字符串拆分为字符串数组。

这个方法会将一个字符串按照指定的分隔符拆分成多个字符串,并将这些字符串存储在一个字符串数组中。Split 方法有多个重载版本,其中最常用的版本接受一个字符数组作为分隔符。

例如:

string str = "one,two,three,four,five";
string[] arr = str.Split(',');
Console.WriteLine("Original string: " + str);
Console.WriteLine("Split strings:");
foreach (string s in arr)
Console.WriteLine(s);
csharp-image10

首先定义了一个字符串变量 str,并初始化为 "one,two,three,four,five"。

然后我们调用了 Split 方法来将这个字符串按照逗号 , 进行拆分,并将拆分后的结果存储在一个字符串数组 arr 中。

最后,我们使用 foreach 循环遍历这个字符串数组,并逐个输出拆分后的子字符串。

关于转义字符

\t制表位、\n换行都很熟悉了

但是

如果需要输出hello"world

需要Console.WriteLine("hello\"world");

给出个\用来转义 表示后续符号是要输出的

但如果想输出hello\tworld

这个\该如何输出?

同上$,加上@符号

Console.WriteLine(@"hello\tworld");

表示字符串中转义字符不予自动转义处理

StringBuilder

用于处理大量字符串拼接的操作。

在c里面已经了解过 字符串是在一块特殊的地址存放着的 而上述对string的操作其实每次都会生成一个新的string 而不对原本string进行修改 这就可能存在一个问题 操作太多了的话 就占据了很多的空间 这并不经济 如果能对原有字符串进行修改就好了——

与直接使用 string 类型进行字符串拼接不同,StringBuilder 在拼接字符串时不会重复创建新的字符串实例,而是在现有的字符串缓冲区中进行修改。

这样可以提高字符串拼接的效率,避免频繁地创建和销毁字符串实例,从而减少了内存的占用和垃圾回收的压力。

(有线性表那味)

StringBuilder sb = new StringBuilder();
sb.Append("Hello");
sb.Append(" ");
sb.Append("world");
sb.Append("!");
Console.WriteLine(sb.ToString());

创建一个 StringBuilder 对象 sb,通过调用 Append 方法来向 sb 对象中添加字符串,调用 ToString 方法将 sb 对象转换成一个普通的字符串,并输出到控制台中

StringBuilder 类还提供了许多其他的方法,

Insert 方法

在 StringBuilder 对象的指定位置插入一个字符串。

StringBuilder sb = new StringBuilder("Hello world!");
sb.Insert(5, ", ");
Console.WriteLine(sb.ToString()); // 输出:Hello, world!

Replace 方法

在 StringBuilder 对象中替换指定的字符串

StringBuilder sb = new StringBuilder("Hello world!");
sb.Replace("world", "C#");
Console.WriteLine(sb.ToString()); // 输出:Hello C#!
Remove 方法

从 StringBuilder 对象中移除指定位置开始的指定数量的字符

StringBuilder sb = new StringBuilder("Hello world!");
sb.Remove(5, 6);
Console.WriteLine(sb.ToString()); // 输出:Hello!

Clear方法

清空当前 StringBuilder 对象中的内容。

StringBuilder sb = new StringBuilder("Hello world!");
sb.Clear();
Console.WriteLine(sb.ToString()); // 输出:(空字符串)

Length 属性:

获取或设置当前 StringBuilder 对象中字符的总数。

StringBuilder sb = new StringBuilder("Hello world!");
Console.WriteLine(sb.Length); // 输出:11
sb.Length = 5;
Console.WriteLine(sb.ToString()); // 输出:Hello

Capacity 属性:

获取或设置当前 StringBuilder 对象的容量。

(如果需要向 StringBuilder 对象中添加大量的字符串,可以使用 Capacity 属性预先分配足够的内存,以提高程序的效率)

StringBuilder sb = new StringBuilder();
Console.WriteLine(sb.Capacity); // 输出:16
sb.Capacity = 32;
sb.Append("This is a long string that requires more capacity.");
Console.WriteLine(sb.Capacity); // 输出:64

ToString方法:

将当前 StringBuilder 对象转换为一个字符串。

StringBuilder sb = new StringBuilder("Hello world!");
string str = sb.ToString();
Console.WriteLine(str); // 输出:Hello world!
AppendFormat(string format, params object[] args):

按照指定的格式向 StringBuilder 对象中添加格式化字符串。

StringBuilder sb = new StringBuilder();
sb.AppendFormat("The answer is {0}.", 42);
Console.WriteLine(sb.ToString()); // 输出:The answer is 42.
AppendLine(string value):

向 StringBuilder 对象中添加一个新行,并附加指定的字符串。

AppendLine():

向 StringBuilder 对象中添加一个新行

StringBuilder sb = new StringBuilder();
sb.AppendLine("First line");
sb.AppendLine("Second line");
sb.AppendLine();
sb.AppendLine("Fourth line");
Console.WriteLine(sb.ToString());
// 输出:
// First line
// Second line
//
// Fourth line

需要注意的是,这些方法都会在原始的 StringBuilder 对象上进行修改,而不是返回一个新的 StringBuilder 对象。因此,在进行这些操作时,应该注意不要改变原始字符串的长度和结构。

Math

Math.PI:

获取圆周率 π 的近似值。

double pi = Math.PI;
Console.WriteLine(pi); // 输出:3.14159265358979

常用的数学函数:

double num = 2.5;
// 平方根
double sqrt = Math.Sqrt(num);
Console.WriteLine(sqrt); // 输出:1.58113883008419
// 绝对值
double abs = Math.Abs(-num);
Console.WriteLine(abs); // 输出:2.5
// 最大值和最小值
double max = Math.Max(num, 3.5);
Console.WriteLine(max); // 输出:3.5
double min = Math.Min(num, 1.5);
Console.WriteLine(min); // 输出:1.5
// 取整
double round = Math.Round(num);
Console.WriteLine(round); // 输出:2
double ceil = Math.Ceiling(num);
Console.WriteLine(ceil); // 向上取整 输出:3
double floor = Math.Floor(num);
Console.WriteLine(floor); // 向下取整 输出:2

需要注意的是

Math.Round 方法是个四舍五入的函数

对于5有两种规则

可以通过 MidpointRounding 枚举类型进行控制

它具有两个选项:

ToEven:默认值,表示使用“四舍六入五成双”的规则(“银行家舍入规则”)。当要舍弃的数字为 5 时,如果它前面的数字是偶数,则向下舍弃,如果它前面的数字是奇数,则向上进位

AwayFromZero:表示始终向远离零的方向舍入。

double x = 5.5;
Console.WriteLine(Math.Round(x, 0, MidpointRounding.ToEven)); // 输出 6

因为要舍弃的数字是 5,而它前面的数字 5 是奇数,所以使用 ToEven 舍入规则时会向上进位,即结果为 6。

double x = 5.5;
Console.WriteLine(Math.Round(x, 0, MidpointRounding.AwayFromZero)); // 输出 6

AwayFromZero 舍入规则表示始终向远离零的方向舍入,而 5.5 距离零更远的方向是向上,所以结果也是 6。

double x = 4.5;
Console.WriteLine(Math.Round(x, 0, MidpointRounding.ToEven)); // 输出 4

因为要舍弃的数字是 5,而它前面的数字 4 是偶数,所以使用 ToEven 舍入规则时会向下舍弃,即结果为 4。

double x = 4.5;
Console.WriteLine(Math.Round(x, 0, MidpointRounding.AwayFromZero)); // 输出 5

AwayFromZero 舍入规则表示始终向远离零的方向舍入,而 4.5 距离零更远的方向是向上,所以结果是 5。

指数、对数函数:

double num = 2.0;
// 幂函数
double pow = Math.Pow(num, 3);
Console.WriteLine(pow); // 输出:8
// 自然指数函数
double exp = Math.Exp(num);
Console.WriteLine(exp); // 输出:7.38905609893065
// 对数函数
double log = Math.Log(num, 10);
Console.WriteLine(log); // 输出:0.301029995663981
or
double log10 = Math.Log10(num);
Console.WriteLine(log10); // 输出:0.301029995663981

三角函数:

double angle = Math.PI / 6;
// 正弦函数
double sin = Math.Sin(angle);
Console.WriteLine(sin); // 输出:0.5
// 余弦函数
double cos = Math.Cos(angle);
Console.WriteLine(cos); // 输出:0.866025403784439
// 正切函数
double tan = Math.Tan(angle);
Console.WriteLine(tan); // 输出:0.577350269189626

数组

C# 中的数组是一种数据结构,可以用来存储一组相同类型的元素。

与 C 语言中的数组不同,C# 中的数组是对象,可以进行面向对象的操作。

C# 中的数组可以是单维、多维或者交错数组。

其中,单维数组是最简单的一种数组,它包含一组相同类型的元素,可以使用整数索引来访问其中的元素。

int[] nums = new int[5];  // 定义包含 5 个整数的数组
nums[0] = 10;  // 设置数组的第一个元素为 10
nums[1] = 20;  // 设置数组的第二个元素为 20
int x = nums[0];  // 获取数组的第一个元素

C# 中的数组使用整数索引来访问其中的元素,索引是从 0 开始的整数值,表示数组中元素的位置。例如,数组的第一个元素的索引是 0,第二个元素的索引是 1,以此类推。使用方括号 [] 运算符来访问数组元素。例如,nums[0] 表示访问数组中的第一个元素。(可以就直接当成下标理解)

理解

C# 中的数组与 C 语言中的数组有所不同。在 C 语言中,数组是一个指向连续内存空间的指针,可以通过指针加上一个偏移量来访问数组中的元素。而在 C# 中,数组是一个对象,由 CLR(Common Language Runtime)进行内存管理。我们不能直接对数组进行指针操作,只能通过索引来访问数组中的元素。

csharp-image11

如图 在C 语言中了解到,数组是在当前位置(栈区)分配一段连续的内存空间,而且我们可以直接对这个内存空间进行指针操作来访问数组中的元素。而字符串则是在内存的一处较高的地址中存储,然后我们在程序中定义一个指针变量来指向这个字符串的地址,通过指针操作来访问字符串的内容。

csharp-image12

在 C# 中,数组和字符串都是对象,它们的内存分配和管理方式都是由 CLR

进行管理的。在 CLR 中,对象都是在堆中分配空间,然后返回对象的引用,我们使用这个引用来访问对象。因此,C# 中的数组也是在堆中分配一段连续的内存空间来存储数组元素,然后返回数组对象的引用,我们使用这个引用来访问数组中的元素。

因此,可以把 C# 中的数组看作是在另一处高地址(堆区)申请空间,然后在当前位置(栈区)给出一个引用的形式来访问这个数组对象,类似于 C 语言中的字符串。

指针基本被封装成了引用 数组自然而然也从const指针变成引用类型

声明

一维

在 C# 中,数组的长度必须在声明数组时指定,并且不能使用变量作为数组长度。这意味着不能像在 C 语言中那样声明数组时直接指定长度。

在 C# 中,要声明一个长度为 10 的整型数组,需要这样写:

int[] a = new int[10];

new int[10] 表示在内存中分配一个长度为 10 的整型数组,并将其初始化为默认值(在这种情况下,所有元素都将被初始化为 0) 这也带有类的味道

注意,这里的 new 关键字表示在堆上动态分配内存,因为在 C# 中,所有对象(包括数组)都是在堆上分配的。

还可以使用隐式数组初始化语法来声明和初始化数组:

int[] a = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

在这种情况下,编译器会自动推断数组的长度,并在内存中分配一个长度为 10 的整型数组。

还可以使用数组初始化器来声明和初始化数组:

int[] a = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

这里的 new int[] 表示在内存中分配一个整型数组,并将其初始化为花括号中指定的值。这里的数组长度也是由数组初始化器中的元素数量确定的。

总之,在 C# 中,声明数组时必须指定数组的长度,而且长度不能是变量。

字符串数组

字符串数组是一种特殊的数组类型,用于存储多个字符串对象。

在 C# 中,可以使用以下语法来声明和初始化一个字符串数组:

string[] names = new string[] { "Alice", "Bob", "Charlie" };

这个语法中,string[] 表示声明一个字符串数组类型的变量,names 是变量的名称,new string[] { ... } 表示创建一个字符串数组,并用花括号中的字符串对象来初始化数组元素。

也可以使用以下语法来简化上面的声明:

string[] names = { "Alice", "Bob", "Charlie" };

在使用字符串数组时,可以使用索引来访问数组元素。比如,要访问第一个元素,可以使用 names[0],要访问第二个元素,可以使用 names[1],以此类推。另外,也可以使用 Length 属性来获取数组的长度,比如 names.Length。

二维数组

二维数组是一种具有两个维度的数组类型,也被称为矩阵或表格。

在 C# 中,可以使用以下语法来声明和初始化一个二维数组:

int[,] matrix = new int[3, 4];

这个语法中,int[,] 表示声明一个二维数组类型的变量,matrix 是变量的名称,new int[3, 4] 表示创建一个 3 行 4 列的二维数组,并用默认值 0 来初始化数组元素。

在使用二维数组时,可以使用两个索引来访问数组元素。比如,要访问第二行第三列的元素,可以使用 matrix[1, 2](注意索引从 0 开始)。另外,也可以使用 GetLength(0) 和 GetLength(1) 方法来获取数组的行数和列数,比如 matrix.GetLength(0) 和 matrix.GetLength(1)。

三维数组

三维数组是一种具有三个维度的数组类型,也被称为立方体或立体数组。在 C# 中,可以使用以下语法来声明和初始化一个三维数组:

int[,,] cube = new int[3, 4, 5];

这个语法中,int[,,] 表示声明一个三维数组类型的变量,cube 是变量的名称,new int[3, 4, 5] 表示创建一个 3 层 4 行 5 列的三维数组,并用默认值 0 来初始化数组元素。

在使用三维数组时,可以使用三个索引来访问数组元素。比如,要访问第二层第三行第四列的元素,可以使用 cube[1, 2, 3](注意索引从0开始):

int[,,] cube = new int[3, 4, 5];
cube[1, 2, 3] = 42;

定义了一个 3x4x5 的三维数组,然后使用三个索引访问其中的元素。其中,第一个索引对应数组的第一维,第二个索引对应数组的第二维,第三个索引对应数组的第三维。在这个例子中,我们将第二层第三行第四列的元素赋值为42。

需要注意的是,三维数组只是一种特殊的数组,它可以拓展到更高维度的数组。无论是二维、三维,还是更高维度的数组,它们都是以相似的方式声明和访问的。

交错数组

除了多维数组,C# 中还有交错数组的概念。

在 C# 中,交错数组(Jagged array)是一种由一维数组构成的数组,也就是说,每个数组元素都是一个一维数组。与多维数组不同的是,交错数组的每个一维数组的长度可以不同。这也是交错数组与多维数组的主要区别。

我们可以使用如下的方式声明和初始化一个交错数组:

int[][] jaggedArray = new int[3][];
jaggedArray[0] = new int[] { 1, 2, 3 };
jaggedArray[1] = new int[] { 4, 5 };
jaggedArray[2] = new int[] { 6, 7, 8, 9 };

定义了一个包含三个一维数组的交错数组,然后分别为每个一维数组赋值。注意,第二个和第三个一维数组的长度是不同的。

与多维数组类似,我们可以使用多重索引访问交错数组的元素。比如,要访问第二个一维数组的第二个元素,可以使用 jaggedArray[1][1]。

需要注意的是,虽然交错数组与多维数组有相似之处,但它们实际上是两种不同的数据结构。

为什么int a[10];变成不合法的了  需要int[] a=new int[10];

在 C# 中,数组声明时需要分开指定数组的名称和长度,这是因为 C# 中的数组是引用类型,它是存储在堆上的对象。当声明一个数组时,实际上是在堆上分配了一段连续的内存空间,然后返回一个指向该内存空间的引用。这个引用可以看作是指向数组对象的指针。

因为数组是对象,所以在声明时必须要指定数组的名称,这样才能在代码中引用该对象。而数组的长度是在运行时才能确定的,所以声明时不能指定长度。

另外,C# 中的数组可以使用关键字 new 来动态地创建数组对象。使用 new 关键字可以在运行时根据需要分配内存空间,从而动态改变数组的大小。这也是与 C 语言中静态数组的区别之一。

总之,C# 中的数组作为引用类型存储在堆上,因此在声明时需要先分开指定数组的名称和长度,以便在堆上分配连续的内存空间,并返回指向该内存空间的引用。同时,C# 中的数组具有动态分配内存空间的特点,可以在运行时根据需要动态地改变数组的大小。

用关键字 new 来创建数组,例如:

int[] arr = new int[10];

也可以在声明数组时不使用 new 关键字来创建数组,例如:

int[] arr = { 1, 2, 3, 4, 5 };

需要注意的是,使用不带 new 关键字的方式创建数组时,数组的长度是根据初始化时提供的元素数量来确定的,因此不能在后续的代码中动态更改数组的大小。同时,这种方式也只适用于静态的初始化情况,如果需要动态地在程序运行时初始化数组,仍然需要使用 new 关键字来创建数组。

日期与时间

在C#中,日期和时间相关的知识包括日期时间的表示、日期时间的格式化、日期时间的计算和操作等。

  1. 日期时间的表示:

在C#中,可以使用DateTime结构来表示日期和时间。DateTime结构提供了一系列属性和方法来操作日期和时间,包括获取年、月、日、时、分、秒等信息。

DateTime now = DateTime.Now; // 获取当前日期和时间
DateTime today = DateTime.Today; // 获取当前日期,时间部分为零点
int year = now.Year; // 获取年份
int month = now.Month; // 获取月份
int day = now.Day; // 获取日期
int hour = now.Hour; // 获取小时
int minute = now.Minute; // 获取分钟
int second = now.Second; // 获取秒钟
  1. 日期时间的格式化:

在C#中,可以使用DateTime的ToString()方法将日期时间按照指定的格式转换为字符串。常见的格式化字符串包括"yyyy-MM-dd"(年-月-日),"HH:mm:ss"(小时:分钟:秒钟)等。

DateTime now = DateTime.Now;
string formattedDate = now.ToString("yyyy-MM-dd"); //格式化日期为"年-月-日"
string formattedTime = now.ToString("HH:mm:ss"); //格式化时间为"小时:分钟:秒钟"
  1. 日期时间的计算和操作:

在C#中,可以使用DateTime结构提供的方法进行日期时间的计算和操作。例如,可以使用Add方法增加指定的时间间隔,使用Subtract方法计算两个日期之间的时间间隔。

DateTime now = DateTime.Now;
DateTime futureDate = now.AddHours(2); // 增加2小时
DateTime pastDate = now.Subtract(TimeSpan.FromDays(3)); // 减去3天
TimeSpan timeSpan = futureDate - now; // 计算时间间隔
int hours = timeSpan.Hours; // 获取小时数
int minutes = timeSpan.Minutes; // 获取分钟数
  1. 日期时间的比较和判断:

在C#中,可以使用DateTime结构提供的比较运算符(如>、<、==等)来比较日期时间的先后顺序。还可以使用DateTime的属性和方法来判断日期时间的特征,如是否为闰年、是否为同一天等。

DateTime date1 = new DateTime(2023, 5, 23);
DateTime date2 = new DateTime(2023, 5, 24);
bool isLeapYear = DateTime.IsLeapYear(2023); // 判断是否为闰年
bool isSameDay = date1.Date == date2.Date; // 判断是否为同一天
bool isAfter = date1 > date2; // 判断date1是否在date2之后

集合

数组和集合都是可以存储一系列数据的数据结构

但集合可以动态地增删元素,大小不固定,且可以存储不同类型的数据。

(集合作为一种数据结构可以使用数组或链表的方式来实现,但大部分集合是使用链式实现的,因为链式存储方式相对于数组存储方式,具有更好的动态性和扩展性,可以更方便地实现添加、删除等操作。另外,链式存储方式也避免了数组的缺点,不需要提前分配空间,可以更灵活地使用内存。不过,链式存储方式的缺点就是访问速度相对慢一些,因为需要通过指针来访问元素。)

csharp-image13

集合是指一组对象的容器,它们具有动态大小和类型安全性,可以存储不同类型的元素,并且可以动态地添加或删除元素,不需要预先声明容器的大小

ArrayList

ArrayList非常类似于数组,也称为数组列表,ArrayList是一个可动态维护的集合。数组的容量是固定的,而ArrayList的容量是可以根据需要自动扩充,它的索引会根据程序的扩展而重新进行分配和调整。ArrayList提供了一系列的方法对其中的元素,进行访问增加,删除的操作。

和数组类似,ArrayList中存储的数据称为元素;ArrayList可以保存的元素就是ArrayList的容量,其默认初始容量为0;可以通过索引访问ArrayList中的元素,索引从0开始。

ArrayList类属于System.Collections命名空间,这个命名空间包含接口和类,这些接口和类定义各种对象(如列表,队列,位数组,哈希表和字典)的集合,ArrayList就属于集合中的一种,因此在使用ArrayList类之前一定要引入System.Collection命名空间

需要注意的是,ArrayList是动态可维护的,因此定义时既可以指定容量,又可以不指定容量。

ArrayList aryLst = new ArrayList();
aryLst.Add("Ada");
aryLst.Add(46);
aryLst.AddRange(new ArrayList() { 119, 143, 132 });
aryLst.Insert(1, 'F');
foreach (object ele in aryLst)
{
    Console.Write(ele+"\t");
}
Console.WriteLine("元素有{0}个,容量为{1}", aryLst.Count, aryLst.Capacity);
aryLst.Remove(143);
foreach (object ele in aryLst)
{
    Console.Write(ele + "\t");
}
Console.WriteLine();
aryLst.RemoveAt(0);
foreach (object ele in aryLst)
{
    Console.Write(ele + "\t");
}
Console.WriteLine();
aryLst[0] = 119;
foreach (object ele in aryLst)
{
    Console.Write(ele + "\t");
}
Console.WriteLine();
int idx1 = aryLst.IndexOf(119);
int idx2 = aryLst.IndexOf(119,1);
int idx3 = aryLst.LastIndexOf(119);
bool exist = aryLst.Contains(9);
Console.WriteLine("idx1: " + idx1 + "\tidx2: " + idx2 + "\tidx3: " + idx3);
Console.WriteLine("9" + (exist ? "存在" : "不存在"));
aryLst.Sort();
foreach (object ele in aryLst)
{
    Console.Write(ele + "\t");
}
Console.WriteLine();
aryLst.Reverse();
foreach (object ele in aryLst)
{
    Console.Write(ele + "\t");
}
Console.WriteLine();
aryLst.Clear();
foreach (object ele in aryLst)
{
    Console.Write(ele + "\t");
}
Console.WriteLine();

Hashtable

在ArrayList集合中我们使用索引访问它的元素,但是使用这种方式必须了解集合中某个数据的位置。当ArrayList中的元素变化频繁时,要跟踪某个元素的下标就比较困难了。那么能不能给集合中的每个元素分别起一个有意义的关键字,然后通过关键字来访问其中的元素呢?

C#为我们提供了一种称为Hashtable的数据结构,通常称为哈希表,也有人称为

“字典”。使用字典这个名称是因为其数据构成非常类似于现实生活中的字典。在一本字典中我们通过一个单词查找关于这个单词的更多信息。Hashtable的数据是通过键(Key)和值(value)来组织的。Hashtable也属于System.Collection命名空间,它的每个元素都是一个键/值对

Hashtable ht = new Hashtable();
ht.Add(1, "Ada");
ht.Add("two", "Bob");
foreach (Object key in ht.Keys)
Console.Write(ht[key] + "\t");
Console.WriteLine(ht.Count);
ht[1] = "BOB";
foreach (Object key in ht.Keys)
Console.Write(ht[key] + "\t");
Console.WriteLine();
ht.Remove("two");
foreach (Object key in ht.Keys)
Console.Write(ht[key] + "\t");
Console.WriteLine();
Console.WriteLine((ht.ContainsKey("two") ? "存在" : "不存在"));
Console.WriteLine((ht.ContainsValue("BOB") ? "存在" : "不存在"));

由于Hashtable不能用索引访问,所以遍历一个Hashtable只能用foreach()方法

//遍历Key
foreach (Object obj in enineers.Keys)
{
    Console.WriteLine((string)obj);
}
//遍历value
foreach (Object obj in enineers.Values)
{
    SE se = (SE)obj
    Console.WriteLine(se.Name);
}
//遍历Hashtable还可以采用下面的方式。
foreach (DictionaryEntry en in enineers)
{
    Console.WriteLine(en.Key);
    Console.WriteLine(((SE)en.Value).Name);
}

DictionaryEntry是一个结构,定义可设置或者检索的Hashtable的键/值对

List

在System.Collections.Generic命名空间中定义了许多泛型集合类

这些类可以替代我们前边学习的ArrayList和Hashtable

(泛型集合相对于非泛型集合具有 更好的类型安全性:在编译时就可以确定元素类型,避免了类型转换错误;更高的性能:由于泛型集合在编译时就确定了元素类型,所以可以避免在运行时进行装箱和拆箱操作,提高了性能;更好的可读性和可维护性:使用泛型集合可以使代码更加清晰,易于理解和维护……总之,泛型集合是C#中更加推荐的一种集合类型,因为它们具有更好的类型安全性和更高的性能)

语法:List 对象名 = new List();

“”中的T可以对集合中的元素类型进行约束,T表明集合中管理的元素类型,对集合中的数据类型进行约束

csharp-image14
List<int> list = new List<int>();
list.Add(70);
list.AddRange(new int[] { 80, 90 });
foreach (int i in list)
Console.Write(i + "\t");
Console.WriteLine();
list.Insert(3, 60);
foreach (int i in list)
Console.Write(i + "\t");
Console.WriteLine("数目:" + list.Count);
list.Remove(80);
list.RemoveAt(2);
foreach (int i in list)
Console.Write(i + "\t");
Console.WriteLine();
list.Sort();
foreach (int i in list)
Console.Write(i + "\t");
Console.WriteLine();
list.Reverse();
foreach (int i in list)
Console.Write(i + "\t");
Console.WriteLine();
list.Clear();
foreach (int i in list)
Console.Write(i + "\t");
Console.WriteLine();

Dictionary<K,V>

具有泛型的全部特性,编译时检查类型约束,获取元素时无需类型转换,并且它储存数据的方式和Hashtable类似,也是通过Key/Value(键/值)对元素保存的。

语法:Dictionary<K,V> 对象名 = new Dictionary<K,V>();

说明:Dictionary<K,V>中表示集合中Key的类型,V表示Value的类型,它们的含义和List是相同的。

Dictionary<K,V>和Hashtable的区别:

csharp-image15

Dictionary<K,V>还可以采用以下方式:

foreach (KeyValuePair<string, SE> en in engineers){
Console.WriteLine(en.Key);
Console.WriteLine(en.Value.Name);
}

KeyValuePair<TKey,TValue>是一个泛型结构,定义可设置或者检索的键/值对

Dictionary<int, string> dic = new Dictionary<int, string>();
dic.Add(1, "Ada");
dic.Add(2, "Bob");
foreach (int key in dic.Keys)
{
    Console.Write(dic[key] + "\t");
}
Console.WriteLine("数目:" + dic.Count);
dic.Remove(2);
foreach (int key in dic.Keys)
{
    Console.Write(dic[key] + "\t");
}
Console.WriteLine("数目:" + dic.Count);
dic[1] = "Cat";
foreach (int key in dic.Keys)
{
    Console.Write(dic[key] + "\t");
}
Console.WriteLine("数目:" + dic.Count);
Console.WriteLine((dic.ContainsValue("BOB") ? "存在" : "不存在"));
dic.Clear();
foreach (int key in dic.Keys)
{
    Console.Write(dic[key] + "\t");
}
Console.WriteLine();

泛型的优点主要是在于它的类型安全和代码的可重用性。

首先,泛型集合对其所存储的对象进行了类型约束,只允许存储指定类型的对象。这样可以避免在运行时出现类型转换的错误,提高了程序的可靠性和稳定性。

其次,泛型可以支持任意的数据类型,开发人员可以编写一套通用的方法或类,而不必为每种特定的数据类型单独编写一套代码。这样可以提高代码的可重用性和维护性,同时也可以减少代码的冗余和重复。

例如,使用泛型可以编写一个通用的排序算法,可以对任意类型的数组进行排序,而不必为每种类型编写单独的排序方法。这样可以极大地简化代码的编写和维护,同时提高程序的可读性和可维护性。

另外,泛型还可以提高程序的性能,因为它避免了类型转换的开销。普通的集合数据需要进行装箱和拆箱操作,而泛型则可以直接操作指定类型的数据,不需要进行类型转换,从而提高了程序的执行效率。

然后就是面向对象最常听闻的类了

在我的理解中 因为某些特殊的复杂的事物无法用单一的数据类型去表示 于是产生了结构体 但这些一个个结构体有着很多的共性,并且换到面向对象来说 用类去表示更贴切不过 它甚至还将一些行为动作(函数)集成进去 可以理解为是结构体+函数

类:

用class关键字表示

类是一种面向对象编程的概念,用于封装数据和方法。

可以将类看作是一种自定义类型,包含变量、属性、方法等成员,以及构造函数和析构函数等特殊成员。

在C#中,所有的代码都必须写在类中,即使只有一个函数也要写在类中。

同时,C#中还有一个特殊的类叫做静态类,该类中的成员都必须是静态的,而且不能创建该类的实例对象。

类可以被实例化为对象,每个对象都有自己的成员数据和方法,不同对象之间的数据和方法是相互独立的。

同时,类也可以派生出子类,子类继承了父类的成员,并可以添加自己的成员。

class

是一种模板或蓝图,它定义了一种对象的属性和方法。

通过class,我们可以创建多个具有相同属性和方法的对象实例。

例如,定义一个Person类,包含姓名、年龄和性别等属性,以及吃饭、睡觉和工作等方法。

然后,可以创建多个不同的Person对象,每个对象都具有相同的属性和方法。

class Person{
public string Name { get; set; }
public int Age { get; set; }
public string Gender { get; set; }
public void Eat(){
Console.WriteLine("I am eating.");
}
public void Sleep(){
Console.WriteLine("I am sleeping.");
}
public void Work(){
Console.WriteLine("I am working.");
}
}

object

object 是 C# 中所有类的基类。

换句话说,每个类都可以被看作是 object 类的子类。

因此,object 类的所有成员都可以在子类中使用。

当定义一个变量但不确定它的具体类型时,可以将其声明为 object 类型

object obj = 10;

使用 object 可以帮助我们在不确定变量类型的情况下,避免编译时错误。

但是,在运行时需要对 object 进行类型转换才能获得具体类型的值

int i = (int)obj;
Person person = new Person();
object obj = person;
Person person2 = (Person)obj;

创建一个Person对象person,然后将它赋给object类型的变量obj。将obj转换为Person对象,并将它赋给person2变量

总之,object 是 C# 中最基本的类,所有其他类都是从它派生出来的。它允许我们在不确定变量类型的情况下,避免编译时错误,但在运行时需要进行类型转换才能获得具体类型的值。

namespace/using

当类创建得太多时,往往容易出现命名重复等问题

但他们实际并不是同一个东西 如何区别开来 如何解决?

这就需要一个空间去管理这些类

在一个namespace中,可以定义多个类,这些类的名称不能相同。

同时,namespace还可嵌套定义(即一个namespace可以包含其他的namespace)这种嵌套的方式可以将相关的类放在同一个namespace中,从而方便管理和使用。除此之外,namespace还可以用来避免命名冲突——如果两个类具有相同的名称,但是它们分别定义在不同的namespace中,那么它们的名称就不会冲突,可以同时存在于程序中

namespace MyNamespace1
{
    class MyClass1
    {
        // Class definition
    }
}
namespace MyNamespace2
{
    class MyClass2
    {
        // Class definition
    }
}

该文件中有两个 namespace:MyNamespace1 和 MyNamespace2。

每个 namespace 包含一个类

它们只能在自己的namespace中使用,如果要在其他namespace中使用,需要在其他namespace中进行引用。

using MyNamespace1;
using MyNamespace2;
class Program
{
    static void Main(string[] args)
    {
        MyClass1 obj1 = new MyClass1();
        MyClass2 obj2 = new MyClass2();
    }
}

当然也可以像一开始system那一样 不using

在每次使用时都加上前缀

MyClass1 obj1 = new  MyNamespace1.MyClass1();
MyClass2 obj1 = new  MyNamespace2.MyClass2();

(在using System;后可以直接写 Console.WriteLine)

然后前面有提到过namespace可以嵌套定义

这种嵌套的命名空间结构可以用来更好地组织和管理代码,使代码更加易于维护和扩展。

namespace MyProject
{
    namespace Utilities
    {
        public class Helper
        {
            // Helper class implementation
        }
    }
}

嵌套命名空间的语法与普通命名空间的语法非常相似,只是在嵌套的命名空间名称前加上了父级命名空间的名称,使用 . 进行分隔。

当然,也可以使用 using 关键字来引用嵌套的命名空间,例如:

using MyProject.Utilities;

这将使得 Utilities 命名空间中的类和成员可以在当前文件中直接使用,而无需使用完全限定名称。

其实可以发现 这很像文件夹!

csharp-image16

如果搞不懂命名空间真的可以换个角度 用接触得多的文件夹去理解

这种嵌套的命名空间可以帮助我们更好地组织类,使得代码更加清晰易读。

var、dynamic和object

在C#中,dynamic和object都是用于处理不确定类型的数据的机制,但是它们的使用方式和功能略有不同。

object是C#中的一种数据类型,它是所有类型的基类,可以用来表示任何数据类型。因为它是一个通用类型,所以在使用object类型时需要进行类型转换,这样可能会导致性能损失或者类型转换错误。

dynamic是C#中的一个关键字,它可以使编译器在编译时不确定变量类型,而是在运行时根据变量的实际类型来进行绑定。这种动态类型的特性可以使编程更加灵活,避免了类型转换等问题,但同时也可能会带来一些运行时错误。

使用dynamic可以避免在编译时就要确定变量的类型,可以在运行时才确定变量的类型。而使用object则需要进行类型转换,因此在性能上可能会有所损失。同时,使用dynamic还可以使用一些C#中不支持的操作符,比如运算符重载和动态绑定等。

需要注意的是,使用dynamic时需要谨慎,因为它会使代码更加难以理解和维护,同时也会带来一些运行时错误,因此应该尽量避免在常规编程中使用dynamic。

var a = "Hello";          // 推断类型为 string
dynamic b = "Hello";      // 运行时类型为 string
object c = "Hello";       // 编译时类型为 object,运行时类型为 string
Console.WriteLine(a.ToUpper());  // 输出 "HELLO"
Console.WriteLine(b.ToUpper());  // 运行时调用 string 的 ToUpper 方法,输出 "HELLO"
Console.WriteLine(c.ToUpper());  // 编译时无法调用 string 的 ToUpper 方法,会编译错误

需要这样:

Console.WriteLine(((string)c).ToUpper()); // 输出 HELLO WORLD

使用 var、dynamic 和 object 分别声明了三个变量 a、b 和 c,并且它们都被赋值为一个字符串 "Hello"。

var 关键字根据赋值语句的类型推断出变量 a 的类型为 string,因此我们可以直接调用 string 类型的方法 ToUpper。

dynamic 关键字则在运行时才确定变量 b 的类型,因此我们也可以在运行时调用 string 类型的方法 ToUpper。

object 关键字在编译时确定变量 c 的类型为 object,因此我们无法直接调用 string 类型的方法 ToUpper,需要进行类型转换。

总的来说,var 和 dynamic 都是用来简化代码和提高灵活性的关键字,但是它们的使用也需要注意一些潜在的问题。object 则是 .NET 中所有类型的基类,可以用于存储任何类型的对象,但是需要进行类型转换才能调用具体类型的方法和属性。

method方法

就是函数 不过得加个前缀

是一段可以执行特定任务的代码块,通过使用方法可以提高代码的可重用性和模块化程度。

方法可以带有参数,可以有返回值,也可以没有返回值。

(1)带参数的方法

// 带有两个参数的方法
public int Add(int a, int b)
{
    return a + b;
}
// 调用方法
int result = Add(1, 2); // result = 3

(2)带返回值的方法

// 带返回值的方法
public int GetMax(int[] arr)
{
    int max = arr[0];
    for (int i = 1; i < arr.Length; i++)
    {
        if (arr[i] > max)
        {
            max = arr[i];
        }
    }
    return max;
}
// 调用方法
int[] arr = { 1, 2, 3, 4, 5 };
int max = GetMax(arr); // max = 5

(3)没有参数和返回值的方法

// 没有参数和返回值的方法
public void PrintHello()
{
    Console.WriteLine("Hello!");
}
// 调用方法
PrintHello(); // 输出 "Hello!"

(4)方法重载

方法重载是指在同一个类中定义多个名称相同但参数不同的方法,可以提高代码的可读性和可维护性。

// 方法重载
public int Add(int a, int b)
{
    return a + b;
}
public double Add(double a, double b)
{
    return a + b;
}
// 调用方法
int result1 = Add(1, 2); // result1 = 3
double result2 = Add(1.5, 2.5); // result2 = 4.0

(5)可选参数和命名参数

可使用可选参数和命名参数来定义方法,以便提供更灵活的参数传递方式。

// 可选参数和命名参数
public int Add(int a, int b = 0, int c = 0)
{
    return a + b + c;
}
// 调用方法
int result1 = Add(1); // result1 = 1
int result2 = Add(1, 2); // result2 = 3
int result3 = Add(1, 2, 3); // result3 = 6
int result4 = Add(1, c: 3); // result4 = 4

main方法

跟main函数一样,Main 方法是一个程序的入口点。

它是一个特殊的静态方法,即 static 方法。

当程序启动时,操作系统将调用 Main 方法作为程序的入口点。

static void Main(string[] args)
{
    // 程序的执行逻辑
}

string[] args 参数是命令行参数的字符串数组

程序可以根据命令行参数的不同,做出不同的处理。

那既然它是方法 就需要写在类中

所以 往往这样写

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Hello, world!");
    }
}

构造与析构

构造方法和析构方法是 C# 中的两种特殊方法。

构造方法:

构造方法是用于初始化对象的特殊方法。它们与类同名,没有返回类型,并在创建对象时自动调用。构造方法通常用于初始化对象的成员变量。

在 C# 中,如果没有为类定义构造方法,则编译器将为该类提供一个默认的构造方法。如果定义了构造方法,则必须显式调用基类的构造方法。

class Person
{
    private string name;
    private int age;
    // 默认构造方法
    public Person()
    {
        name = "Unknown";
        age = 0;
    }
    // 自定义构造方法
    public Person(string name, int age)
    {
        this.name = name;
        this.age = age;
    }
}
调用时 Person person1 = new Person("张三",18);
this?

当我们创建一个类的实例时,构造方法会被自动调用,用于初始化对象的数据成员。如果这个类中有多个构造方法,它们之间可能会有一些共同的初始化操作,而这些操作可以被封装在一个构造方法中,其他构造方法可以通过调用这个构造方法来避免重复代码。这个构造方法就可以使用this关键字来调用自身或其他构造方法,达到代码复用的目的。

防冗余

在一个构造函数中调用同一个类的另一个构造函数,可以使用 this 关键字来调用。这样做的好处是可以避免代码冗余,提高代码复用率。

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public Person()
    {
        this.Name = "Unknown";
        this.Age = 18;
    }
    public Person(string name) : this()
    {
        this.Name = name;
    }
    public Person(string name, int age) : this(name)
    {
        this.Age = age;
    }
}

三个构造方法,第一个构造方法中使用了this关键字调用了自身,初始化了Name和Age成员;第二个构造方法使用了this关键字调用了第一个构造方法,初始化了Name成员;第三个构造方法使用了this关键字调用了第二个构造方法,初始化了Age成员。这样就可以通过不同的构造方法来初始化对象的成员变量,从而避免了重复代码。

明确对象

当实例变量与局部变量名称相同时,可以使用 this 来明确指定实例变量,避免命名冲突。

当在类中定义一个变量时,如果变量名与类中的实例变量名相同,编译器就无法区分它们,从而导致代码出现错误。

class Example
{
    private int number;
    public void SetNumber(int number)
    {
        number = number; // 这里会出现问题
    }
}

定义了一个名为 number 的私有实例变量,并在 SetNumber 方法中定义了一个名为 number 的参数。由于变量名相同,编译器无法区分它们,导致在给实例变量赋值时,实际上是将参数值赋给了参数本身。

为了避免这种情况发生,我们可以使用 this 关键字来引用实例变量

class Example
{
    private int number;
    public void SetNumber(int number)
    {
        this.number = number; // 使用 this 关键字引用实例变量
    }
}

用 this.number 引用类中的 number 实例变量,这样就能区分参数和实例变量了

析构方法:

析构方法是用于清理对象的特殊方法。

它们与类同名,前面加上一个波浪线(~),没有参数和返回类型。

析构方法在对象被销毁时自动调用,并且不能显式调用。

在 C# 中,需要注意的是,析构方法只能用于释放非托管资源(例如文件句柄、数据库连接等),而不是托管资源(例如内存)。

class MyClass
{
    private IntPtr handle;
    // 构造方法
    public MyClass()
    {
        handle = ... // 获取非托管资源的句柄
    }
    // 析构方法
    ~MyClass()
    {
        // 释放非托管资源
        CloseHandle(handle);
    }
    [DllImport("kernel32.dll")]
    private static extern bool CloseHandle(IntPtr hObject);
}

使用 P/Invoke 调用 Windows API 函数

"CloseHandle" 关闭一个操作系统对象句柄。

其中 "handle" 是一个表示操作系统对象句柄的字段,它在对象创建时被初始化。在对象被销毁时,析构方法会被调用,并在其中调用 "CloseHandle" 函数来释放句柄所占用的资源。由于 "CloseHandle" 函数是一个 Windows API 函数,因此需要使用 "DllImport" 特性将其导入到 C# 中进行调用。

需要注意的是,析构方法不应该被滥用。因为析构方法的调用是由垃圾回收器来决定的,无法保证它们被及时调用,可能会导致资源泄漏。

C#中,垃圾回收器(Garbage Collector)会负责自动回收不再使用的对象和释放其占用的内存,但有些对象可能会占用非托管资源(如文件句柄、数据库连接等),这些资源需要手动释放。如果不释放这些资源,可能会导致程序内存泄漏,影响程序的性能和稳定性。

为了解决这个问题,C#提供了IDisposable接口和Dispose()方法,使得程序员可以手动释放非托管资源。IDisposable接口定义了一个Dispose()方法,该方法在对象不再使用时被调用,用于释放非托管资源。在使用含有非托管资源的对象时,

应该使用using语句或手动调用Dispose()方法来释放资源。

如果需要在对象被垃圾回收时执行一些清理操作,可以使用终结器(Finalizer),它类似于析构函数,但并不是直接释放资源,而是在对象被垃圾回收之前执行某些清理操作。然而,终结器的使用应该尽量避免,因为它可能会影响程序性能。如果需要手动释放非托管资源,还是应该使用IDisposable接口和Dispose()方法。

先不用管这么多

知道c#里不需要去可以写析构函数就行了

至于这些手动释放怎么用?

使用 P/Invoke 调用 Windows API 函数是一种通过 C# 调用非托管代码的方式。在这段代码中,使用 P/Invoke 调用了 Windows API 函数 "CloseHandle",这个函数用于关闭一个操作系统对象句柄。为了调用这个函数,需要将其导入到 C# 中,使用 "DllImport" 特性将其导入。同时,需要定义一个表示操作系统对象句柄的字段 "handle",并在对象创建时进行初始化。

对于包含非托管资源的对象,C#提供了IDisposable接口和Dispose()方法来进行资源释放。IDisposable接口定义了一个Dispose()方法,用于释放非托管资源。在使用这些对象时,应该使用using语句或手动调用Dispose()方法来释放资源,以避免资源泄漏。

下面是一些例子:

使用 P/Invoke 调用 Windows API 函数:

using System;
using System.Runtime.InteropServices;
class Program
{
    [DllImport("user32.dll", CharSet = CharSet.Auto)]
    public static extern int MessageBox(IntPtr hWnd, String text, String caption, uint type);
    static void Main()
    {
        MessageBox(IntPtr.Zero, "Hello World!", "Caption", 0);
    }
}

使用 IDisposable 接口和 using 语句:

using System;
using System.IO;
class Program
{
    static void Main(string[] args)
    {
        using (StreamWriter writer = new StreamWriter("test.txt"))
        {
            writer.WriteLine("Hello World!");
        }
    }
}

手动调用 Dispose() 方法:

using System;
using System.IO;
class Program
{
    static void Main(string[] args)
    {
        StreamWriter writer = null;
        try
        {
            writer = new StreamWriter("test.txt");
            writer.WriteLine("Hello World!");
        }
        finally
        {
            if (writer != null)
            {
                writer.Dispose();
            }
        }
    }
}

get、set

对于某些属性

比如性别 它的取值应该是男或女

年龄 它的取值应该在0~120

但是在这个类外面 我们并不知道这个限制

如果设什么就取什么的话很不符合常理

所以需要对这些属性进行一个保护——变成私有(private)

private string Gender;
public string gender
{
    get
    {
        return Gender;
    }
    set
    {
        if (value == "男" || value=="女")
        {
            Gender = value;
        }
        else
        {
            Gender = "其他";
        }
    }
}

Gender只能在当前类中进行访问

给出一个公共的gender留给外部进行访问

想要访问Gender必须先经过一遍gender

get 取得Gender的值(在同一个类中 可以访问)

set 添加条件 为Gender赋值

在C#中,get和set是一种特殊的语法,用于访问类的属性(property)。

属性是一种类成员,它提供了一种访问类中私有字段的方式,使得代码更加简洁和易读。属性通常由get和set方法组成,get方法用于获取属性值,set方法用于设置属性值。

get和set方法可以被定义为公共的、私有的、受保护的或受内部保护的,这取决于属性的访问级别。

public int Age { get; set; }
private string Gender;public string gender{ get; set; })

属性只是一种访问私有字段的语法糖,它并不是字段本身。在属性的get和set方法中,可以进行各种逻辑处理,如验证输入值、计算属性值等。因此,属性是一种非常强大的语言特性,可以帮助程序员编写更加简洁、易读和可维护的代码。

静态属性(static attribute)

是指与类关联而不是与类的实例关联的属性。

也就是说,不需要创建类的实例就可以访问静态属性。

静态属性在整个应用程序中只有一份,且在程序运行时会一直存在,直到程序结束才会被销毁。

静态属性通常用于保存类级别的数据,例如计数器,全局配置信息等。

静态属性可以在类定义中使用static关键字来声明,例如:

public class MyClass
{
    public static int MyStaticProperty { get; set; }
}
MyClass a = new MyClass();
a.MyStaticProperty = 1;//报错 不能用实例访问
MyClass.MyStaticProperty = 42;
int value = MyClass.MyStaticProperty;

静态属性不能访问实例级别的成员,因为它们与任何实例无关。

另外,静态属性也不能使用实例方法,因为它们没有与任何实例相关联。

csharp-image17

可以用来计算创建了多少个实例对象

csharp-image18

如果非要让实例去访问到这个静态属性也不是不行

用get访问器呗

既然有静态属性 相应的肯定有静态方法

Console.WriteLine(Math.Sqrt(36));

对于这种 我们并没有去声明一个实例就能直接去使用该方法

正是因为它也是静态的 是直接用类调用的

静态方法(static method)

是指属于类的方法,而不是属于类的实例的方法

它们可以直接从类中调用,而不需要实例化类的对象

静态方法可以用于执行与类相关的操作,而不需要访问实例的状态或实例的属性

在C#中,使用static关键字来声明静态方法

静态方法可以直接在类的定义中声明

例如:

public class MyClass
{
    public static void MyStaticMethod()
    {
        // 静态方法的实现
    }
}

使用类名来调用静态方法

MyClass.MyStaticMethod();

尝试去和一些普通类一样 去定义一个math类的实例

csharp-image19

失败了 这说明它和普通类又是不一样的

静态类别(static class)

static class是一种特殊的类,它只能包含静态成员,不能包含实例成员。

静态类可以用来组织和管理一组静态方法和属性,通常用于提供工具类或扩展方法类

使用static class定义的类在运行时只有一个实例,这个实例由.NET运行时自动创建。因此,静态类不能被实例化,也不能被继承。

静态类中的静态成员可以通过类名直接访问,无需创建类的实例

这些静态成员在内存中只会有一份拷贝,可以被多个实例共享,因此静态类在提供共享代码和数据时非常有用。

当然我们也可以实现这种效果

csharp-image20

只需要在类前加上static关键字即可

csharp-image21

总的来说,静态类在提供工具方法和扩展方法时非常方便,可以避免创建多个实例,节省内存空间。但需要注意的是,静态类中的静态成员不能访问实例成员,因为它们没有实例。

继承与多态:

继承是指子类可以继承父类的属性和方法,并可以在子类中重写父类的方法或添加新的方法,通过继承可以实现代码的复用和减少冗余。

通过冒号(:)来实现继承,子类可以继承父类中的所有非私有成员

例如:

class Animal
{
    public virtual void Speak()
    {
        Console.WriteLine("Animal speaks.");
    }
}
class Dog : Animal
{
    public override void Speak()
    {
        Console.WriteLine("Dog barks.");
    }
}

Dog 类继承了 Animal 类,并重写了父类的 Speak() 方法。

多态是指同一种操作作用于不同的对象,可以有不同的解释,产生不同的执行结果,多态可以增加程序的灵活性和扩展性。

多态可以通过继承和接口实现

例如:

interface IAnimal
{
    void Speak();
}
class Dog : IAnimal
{
    public void Speak()
    {
        Console.WriteLine("Dog barks.");
    }
}
class Cat : IAnimal
{
    public void Speak()
    {
        Console.WriteLine("Cat meows.");
    }
}
class Program
{
    static void Main(string[] args)
    {
        IAnimal animal = new Dog();
        animal.Speak();
        animal = new Cat();
        animal.Speak();
    }
}

Dog 和 Cat 类都实现了 IAnimal 接口,并重写了 Speak() 方法。在 Main() 方法中,animal 变量先实例化为 Dog 类的对象,然后再实例化为 Cat 类的对象,并调用了各自的 Speak() 方法,实现了多态。

附件

📄 Csharp.docx(原 word 笔记):Csharp.docx


⬅️ CSharp 🏠 00-编程语言 ➡️ 资源