前言
如果你有以上的基础,你就不用看,如果没有,你就去看
这里主要就是为了防止石山+怎么使用容器,以及超绝C#独有委托,由于C#的委托是独有的,所以会特意讲的比较细,不过其他的看一下即可(如果有py、java、js基础的看除委托以外的会很熟悉)
如果有问题可以在评论区留言
泛型
定义
泛型本质上是参数化类型的技术,好吧,这样说,其实讲起来很抽象的
总之就理解是一个模板类型得了(换言之,这个在一定程度上可以降耦合度,当然,是类型耦合),因为这玩意(原链接:泛型方法 - C# | Microsoft Learn):
泛型向 .NET 引入了类型参数的概念。 泛型使得可以设计类和方法,将一个或多个类型参数的定义推迟到在代码中使用该类或方法时。 例如,通过使用泛型类型参数
T,可以编写一个供其他客户端代码使用的单个类,从而避免了运行时强制转换或装箱操作的成本或风险
由于泛型算是一个比较大的概念,所以就可以套类、方法、接口等
这里就额外提到泛型约束这个玩意了,我会在作用之后详细带入这个概念
以防某些人不懂耦合度
看到这里,应该是不了解耦合度是什么玩意的吧(如果懂的就可以跳过),所以我在这里就额外告诉你:
耦合度就是某模块(类)与其它模块(类)之间的关联、感知和依赖的程度,是衡量代码独立性的一个指标,也是软件工程设计及编码质量评价的一个标准,如果是高耦合度的话,就出现动一个参数而动全部的情况,这种情况将会是灾难性的(非常严重!)
这里就不得不说java语言里Spring框架了(有点偏但达到目的就行),Spring最开始就是为了降耦合度而出现的玩意,当然这里只是为了明确概念而使用Spring做例子,剩下就不细讲了
作用
第一点,降低耦合度,在定义部分讲过了,我就不再赘述
第二点,提高安全性,其实在泛型之前还有个方法,是使用object的,但毕竟object算是所有类的基类,就相当于所有类的父亲,所以就会有类型不安全的问题,总之就是这样
第三点,增加效率,好吧,又是优化
关于类型不安全
在 C# 中,类型安全(Type Safety)是指程序在编译时和运行时对数据类型的严格检查,确保操作的数据类型始终符合预期,避免因类型错误导致未定义行为或崩溃。
类型安全的代码能有效预防如“将字符串当作整数操作”等非法行为。
额外声明:这里面的object并不是js中的var,他不能随便变,类型不匹配会导致崩溃的
泛型约束
定义
这玩意就相当于sql里的约束,就是限制的手段,主要分俩种:一种是new约束,一种是where约束:
new约束:
这个么,就是主要用于在泛型声明中约束可能用作类型参数的参数的类型
(原文档:new 约束 - C# reference | Microsoft Learn)
约束
new指定泛型类或方法声明中的类型参数必须具有公共无参数构造函数。 若要使用new约束,类型不能是抽象的。
where约束:
where这个玩意和sql的where很像,但不一样,这里更多是用来限制参数范围的
(原文档:where(泛型类型约束) - C# reference | Microsoft Learn):
泛型定义中的
where子句指定对用作泛型类型、方法、委托或本地函数中类型参数的参数类型的约束。 约束可指定接口、基类或要求泛型类型为引用、值或非托管类型。 约束声明类型参数必须具有的功能,并且约束必须位于任何声明的基类或实现的接口之后。
where约束这玩意可以约束的地方很多,如果有人愿意详细去学,这里有链接:类型参数的约束 - C# | Microsoft Learn
示例
泛型类/方法
这里就只做了简单演示,直接看代码吧:
namespace Sixth
{
internal class Program
{
static void Main(string[] args)
{
GenericClass<int> intInstance = new GenericClass<int>(50); //创建泛型类的实例,类型参数为int
GenericClass<String> StrIntance = new GenericClass<String>("Hello, Generics!"); //创建泛型类的实例,类型参数为string
Console.WriteLine(intInstance.GetGenericMember()); //调用泛型方法,输出int类型的值
Console.WriteLine(StrIntance.GetGenericMember()); //调用泛型方法,输出string类型的值
Console.WriteLine("Before Switch:");
int a = 10, b = 20;
String strA = "First", strB = "Second";
Console.WriteLine($"a = {a}, b = {b}"); //输出:a = 10, b = 20
Console.WriteLine($"strA = {strA}, strB = {strB}"); //输出:strA = First, strB = Second
Console.WriteLine("After Switch:");
intInstance.Switch<int>(ref a, ref b); //调用泛型方法,交换int类型的值
StrIntance.Switch<String>(ref strA, ref strB); //调用泛型方法,交换string类型的值
Console.WriteLine($"a = {a}, b = {b}"); //输出:a = 20, b = 10
Console.WriteLine($"strA = {strA}, strB = {strB}"); //输出:strA = Second, strB = First
}
}
public class GenericClass<T> //泛型类,T是类型参数
{
private T genericMember; //泛型成员变量
public GenericClass(T value) //构造函数
{
genericMember = value; //初始化泛型成员变量
}
public T GetGenericMember() //泛型方法
{
return genericMember; //返回泛型成员变量
}
public void Switch<T> (ref T a,ref T b) //另一个泛型方法,使用不同的类型参数A,交换两个变量的值
{
T temp = a;
a = b;
b = temp;
}
}
}
ref就是改A和B的,具体可看我之前的文章:C#方法+简单的计算器实例|真理の小屋,这里不再赘述
这里就直接输出结果了(主要是用照片还是太浪费服务器内存了):
50
Hello, Generics!
Before Switch:
a = 10, b = 20
strA = First, strB = Second
After Switch:
a = 20, b = 10
strA = Second, strB = First
如果不相信结果,你可以用C#的ide试试
接口
好吧,这是接口版本:
namespace Sixth
{
internal class Program
{
static void Main(string[] args)
{
// 创建泛型类的实例
Family<String> StringRepo = new Family<String>();
Console.WriteLine(StringRepo); // 输出类型信息
StringRepo.Add("Hello");
StringRepo.Add("World");
Console.WriteLine(StringRepo.Get(0)); // 输出 "Hello"
Console.WriteLine(StringRepo.Get(1)); // 输出 "World"
// 创建泛型类的实例
Family<int> IntRepo = new Family<int>();
Console.WriteLine(IntRepo); // 输出类型信息
IntRepo.Add(114);
IntRepo.Add(514);
Console.WriteLine(IntRepo.Get(0)); // 输出 114
Console.WriteLine(IntRepo.Get(1)); // 输出 514
}
}
//泛型接口
public interface Human<T>
{
void Add(T item);
T Get(int id);
}
//实现泛型接口
public class Family<T> : Human<T>
{
private List<T> items = new List<T>();
public void Add(T item)
{
items.Add(item);
}
public T Get(int id)
{
return items[id];
}
}
}这里简单用了下列表,反正马上讲到这个,我就直接用了
这里是结果:
Sixth.Family`1[System.String]
Hello
World
Sixth.Family`1[System.Int32]
114
514
同样,若不相信可以自测下
约束
这里是约束,但由于篇幅问题+作者学的没那么精,这里就只演示其中一种:
namespace Sixth
{
internal class Program
{
static void Main(string[] args)
{
Family<string> family = new Family<string>();
family.Add("爸爸");
family.Add("妈妈");
family.Add("儿子");
//family.Add(null); // 编译错误:类型参数 'string' 必须是非空类型
Console.WriteLine(family.Get(0));
Console.WriteLine(family.Get(1));
Console.WriteLine(family.Get(2));
}
}
//约束
public class Family<T> where T : notnull
{
private List<T> items = new List<T>();
public void Add(T item)
{
items.Add(item);
}
public T Get(int id)
{
return items[id];
}
}
}
这里是结果:
爸爸
妈妈
儿子
可算是处理完泛型了,接下来就是搞容器了
容器
定义
容器是用来存储和管理的对象,对,定义这一块就这么多内容,不过细分容器的种类很多,总共分为以下几种(这里主要讲常用的):
List<T>(动态数组)、Dictionary<TKey, TValue>(键值对集合)、HashSet<T>(唯一元素集合)、Queue<T>(队列)、Stack<T>(栈)
说实话,有点骇人,不过我还是决定一个一个讲:
由于篇幅问题,接下来的用法(示例)里的用法都在注释里,请谅解
List<T>(动态数组)
定义
数组么,其实和java那些差不多,不过对于新手我还是简单解释下:
数组是一个存储相同类型元素的固定大小的顺序集合,可以通过 new 关键字来创建和初始化
用法(示例)
namespace Sixth
{
internal class Program
{
static void Main(string[] args)
{
//数组初始化器的新语法(C# 12.0及更高版本)
List<String> items = new() { "apple", "banana", "cherry" };
ConsoleList(items,"newitem"); //输出集合内容
//数组初始化器的旧语法(C# 11.0及更低版本)
List<String> oldItems = new List<String> { "apple", "banana", "cherry" };
ConsoleList(oldItems,"olditem"); //输出集合内容
//以上List<String>都可以用var替代
//以下是数组的添加(List<T>常用操作示例)
var numbers = new List<int>();
ConsoleList(numbers,"init numbers"); //输出集合内容
numbers.Add(1); //在末尾添加元素1
numbers.Add(2); //在末尾添加元素2
numbers.Add(3); //在末尾添加元素3
ConsoleList(numbers, "add"); //输出集合内容
numbers.Insert(2, 4); //在索引2处插入元素4
ConsoleList(numbers, "insert"); //输出集合内容
numbers.Remove(2); //移除元素2
ConsoleList(numbers, "remove"); //输出集合内容
numbers.Sort(); //对集合进行排序(默认升序)
ConsoleList(numbers, "sort"); //输出集合内容
numbers.Sort((a, b) => b - a); //使用自定义比较器进行降序排序
ConsoleList(numbers, "custom sort"); //输出集合内容
}
//创建一个泛型方法,用于打印集合中的每个元素(主要省事,便于读者能直接理解)
public static void ConsoleList<T>(IEnumerable<T> value,String valuebefore)
{
Console.WriteLine($"输出集合 {valuebefore} 内容:");
foreach (var item in value)
{
Console.Write(item + " ");
}
Console.WriteLine();
}
}
}这里是结果:
输出集合 newitem 内容:
apple banana cherry
输出集合 olditem 内容:
apple banana cherry
输出集合 init numbers 内容:
输出集合 add 内容:
1 2 3
输出集合 insert 内容:
1 2 4 3
输出集合 remove 内容:
1 4 3
输出集合 sort 内容:
1 3 4
输出集合 custom sort 内容:
4 3 1
这便是集合里最基础的,接下来也是同样基础的键值对集合(或者说字典)
Dictionary<TKey, TValue>(键值对集合)
定义
经典Dictionary,看名字就知道这是字典(
其实这个玩意就是键和值的集合,其中TKey就是键,TValue就是值,其实我讲也没办法讲太明白,微软官方那边也只有一句话概括,我就直接上示例了
用法(示例)
namespace Sixth
{
internal class Program
{
static void Main(string[] args)
{
//字典/键值对集合(Dictionary)的使用
Dictionary<string, int> dict = new Dictionary<string, int>();
PrintDictionary(dict,"init");
dict["one"] = 1; // 添加键值对
dict["two"] = 2; // 添加键值对
PrintDictionary(dict,"add");
dict["one"] = 11; // 修改键值对
PrintDictionary(dict,"modify");
dict.Remove("two"); // 删除键值对
PrintDictionary(dict,"remove");
dict["two"] = 2; // 重新添加键值对
dict["two"] += 3; // 修改键值对
PrintDictionary(dict,"re-add-modify");
}
static void PrintDictionary(Dictionary<string, int> dictionary,String value)
{
Console.WriteLine(value);
foreach (var kvp in dictionary)
{
Console.WriteLine($"Key: {kvp.Key}, Value: {kvp.Value}");
}
Console.WriteLine("-----");
}
}
}这里是结果:
init
-----
add
Key: one, Value: 1
Key: two, Value: 2
-----
modify
Key: one, Value: 11
Key: two, Value: 2
-----
remove
Key: one, Value: 11
-----
re-add-modify
Key: one, Value: 11
Key: two, Value: 5
-----
经典且最常用的俩个讲完了,接下来就是字典的兄弟哈希集了:
HashSet<T>(唯一元素集合)
定义
这个就是哈希集,我不好说,微软官方也只给了一句话(原链接:HashSet<T> 类 (System.Collections.Generic) | Microsoft Learn)
表示一组值
可能会有人觉得这和字典很像(废话,都是从哈希表里出来的),我在这里还是列举几个区别了:
区别
字典和哈希集的区别:
内存占用大小:字典大、哈希集较小
存储内容:字典存键和值(键1,值1/多)、哈希集只存值(值1)
差不多了,好在哈希集的用法比前俩少很多,我就直接在代码里呈现出来了
用法(示例)
namespace Sixth
{
internal class Program
{
static void Main(string[] args)
{
//哈希集
var hashSet = new HashSet<string> { "apple", "banana", "cherry" };
PrintHashSet(hashSet,"init HashSet");
//添加元素
hashSet.Add("date");
PrintHashSet(hashSet,"after Add date");
//尝试添加重复元素
hashSet.Add("date");
PrintHashSet(hashSet, "after Add Repeat date");
//移除元素
hashSet.Remove("banana");
PrintHashSet(hashSet,"after Remove banana");
//检查元素是否存在
bool containsApple = hashSet.Contains("apple");
Console.WriteLine($"Contains apple: {containsApple}");
}
//打印哈希集内容
static void PrintHashSet(HashSet<string> set,string msg)
{
Console.WriteLine(msg + ":");
foreach (var item in set)
{
Console.WriteLine(item);
}
Console.WriteLine("------");
}
}
}这里是结果:
init HashSet:
apple
banana
cherry
------
after Add date:
apple
banana
cherry
date
------
after Add Repeat date:
apple
banana
cherry
date
------
after Remove banana:
apple
cherry
date
------
Contains apple: True
差不多了,接下来就是经典队列和栈
Queue<T>(队列)
定义
最典的来了,这个就是队列,队列是一个线性数据结构,最典型的还是他的“先进先出”的特点
当然这玩意也可以用数组来模拟这个玩意(也挺好模拟的),不过既然有轮子为啥还要自己造轮子呢?
不过笔者为了方便演示队列的基本特性,在这里用数组简单模拟了下:
数组模拟(如果不愿看可略过)
namespace Sixth
{
internal class Program
{
static void Main(string[] args)
{
//列表模拟队列
List<int> Queue = new List<int>() { 1, 2, 3 };
//入队
Enqueue(Queue, 4);
Enqueue(Queue, 5);
Console.WriteLine("入队后队列内容:");
PrintList(Queue);
//出队
int dequeuedItem = Dequeue(Queue);
Console.WriteLine($"\n出队元素:{dequeuedItem}");
Console.WriteLine("出队后队列内容:");
PrintList(Queue);
}
//打印列表内容
static void PrintList(List<int> list)
{
foreach (var item in list)
{
Console.WriteLine(item);
}
}
//入队方法
static void Enqueue(List<int> queue, int item)
{
queue.Add(item);
}
//出队方法
static int Dequeue(List<int> queue)
{
if (queue.Count == 0)
{
throw new InvalidOperationException("队列为空,无法出队。");
}
int item = queue[0];
queue.RemoveAt(0);
return item;
}
}
}这里是结果:
入队后队列内容:
1
2
3
4
5
出队元素:1
出队后队列内容:
2
3
4
5
用法(示例)
namespace Sixth
{
internal class Program
{
static void Main(string[] args)
{
//队列
var queue = new Queue<int>();
//入队(初始化)
queue.Enqueue(1);
queue.Enqueue(2);
queue.Enqueue(3);
Console.WriteLine("队列中的元素有:");
//输出
PrintQueue(queue,"init");
//入队
queue.Enqueue(9);
PrintQueue(queue,"push 9:");
queue.Enqueue(10);
PrintQueue(queue,"push 10:");
//出队
PrintQueue(queue,"pop " + queue.Dequeue()+":");
PrintQueue(queue,"pop " + queue.Dequeue() + ":");
//统计个数
Console.WriteLine("队列中元素的个数为:"+queue.Count);
}
//打印队列中的元素
static void PrintQueue(Queue<int> queue,string msg)
{
Console.WriteLine(msg);
foreach (var item in queue)
{
Console.Write(item.ToString()+" ");
}
Console.WriteLine();
Console.WriteLine("------");
}
}
}这里是结果:
队列中的元素有:
init
1 2 3
------
push 9:
1 2 3 9
------
push 10:
1 2 3 9 10
------
pop 1:
2 3 9 10
------
pop 2:
3 9 10
------
队列中元素的个数为:3
接下来就是栈了
Stack<T>(栈)
定义
和队列一样典的东西来了
其他基本上和队列一样,只不过栈这玩意是遵循后进先出的规则,当然,同样也能够通过动态数组演示(这里我还是不演示了,主要累)
用法(示例)
namespace Sixth
{
internal class Program
{
static void Main(string[] args)
{
//栈
Stack<int> stack = new Stack<int>();
//入栈
stack.Push(1);
stack.Push(2);
stack.Push(3);
PrintStack(stack, "初始栈内容:");
//出栈
PrintStack(stack,"出栈了,栈内容:"+stack.Pop());
//查看栈顶元素
int peeked = stack.Peek();
Console.WriteLine($"栈顶元素:{peeked}");
//统计栈内元素个数
Console.WriteLine($"栈内元素个数:{stack.Count}");
}
//打印栈内元素
static void PrintStack<T>(Stack<T> stack,string msg)
{
Console.WriteLine(msg);
foreach (var item in stack)
{
Console.WriteLine(item);
}
Console.WriteLine("------");
}
}
}这里是结果:
初始栈内容:
3
2
1
------
出栈了,栈内容:3
2
1
------
栈顶元素:2
栈内元素个数:2
可算是完事了,那就顺带讲讲匿名这玩意了(算是放松)
匿名
定义
这里的隐匿是一种简化定义的方法,不过正因为简化了定义,所以基本上就是不可变的
(虽然逻辑上可能不太对,但作者喜欢以好处和代价的方式讲述,请读者谅解)
在这里我可以适当使用点微软文档(原链接:匿名类型 - C# | Microsoft Learn):
匿名类型提供了一种方便的方法,可用来将一组只读属性封装到单个对象中,而无需首先显式定义一个类型。 类型名称由编译器生成,在源代码级别不可用。 每个属性的类型由编译器推断。
可结合使用 new 运算符和对象初始值设定项创建匿名类型。 有关对象初始值设定项的详细信息,请参阅对象和集合初始值设定项。
显然,C#的匿名就是抄JS的对象的(如果你简单看示例就知道了),不过C#就算是抄JS还是强类型语言,因此匿名仍然和JS的有一定区别,在这里,我简单的写了一点点区别:
区别
C#的匿名就算是用var,他也是强类型,编译的时候就是会直接给你个超级简单的密封类,是不可变的;
而JS是弱类型,定义了之后还是会超级变变变的;
而且匿名只是其中一种方法,而JS的对象是核心,这种性质还是不一样的
好了,区别讲完了,那就做示例了:
示例
namespace Sixth
{
internal class Program
{
static void Main(string[] args)
{
//隐匿的类型
var v = new { Name = "隐匿的类型", Age = 18 };
Console.WriteLine(v); //输出:{ Name = 隐匿的类型, Age = 18 }
Console.WriteLine(v.Name); //输出:隐匿的类型
Console.WriteLine(v.Age); //输出:18
}
}
}这里是结果:
{ Name = 隐匿的类型, Age = 18 }
隐匿的类型
18
匿名差不多了,接下来就是非常头疼的一个玩意----委托(delegate)
委托
定义
好吧,这货还是一定得要好好学学,这个b就是C#独有的特性----委托
由于这个特性是特有的,所以我先搬出那万能的微软文档,便于理解(原连接:在 C# 中处理委托类型 - C# | Microsoft Learn):
委托是一种类型,表示对具有特定参数列表和返回类型的方法的引用。 实例化委托时,可以将委托实例与具有兼容签名和返回类型的任何方法相关联。 可以通过委托实例来调用(或执行)该方法。
委托用于将方法作为参数传递给其他方法。 事件处理程序本质上是通过委托调用的方法。 创建自定义方法时,Windows 控件等类可以在发生特定事件时调用方法。
分类
这里的委托主要分俩类,一类是Action,一个是Func
其中Action是没有返回值的委托,类似于void方法
而Func是有返回值的委托,类似于除void以外的方法
用法(示例)
namespace Sixth
{
internal class Program
{
static void Main(string[] args)
{
//委托
Console.WriteLine("委托Action无返回值:");
Action<string> print = Print => Console.WriteLine(Print); //使用lambda表达式创建一个委托
print("Hello, World!");
blank();
Console.WriteLine("委托Func可以有返回值:");
Func<float, float, float> func = Add; //创建一个Func委托实例,指向Add方法
print(func(3,4).ToString()); //调用委托
blank();
Console.WriteLine("初始化Add:");
MathFunc mathFunc = Add; //创建一个MathFunc委托实例,指向Add方法
mathFunc(10, 5); //调用委托
blank();
Console.WriteLine("之后加Sub方法:");
mathFunc += Sub; //加入Sub方法
mathFunc(2, 3); //调用委托
blank();
Console.WriteLine("之后减Sub方法:");
mathFunc -= Sub; //移除Sub方法
mathFunc(25,3.2f);
blank();
Console.WriteLine("之后加Mul方法:");
mathFunc += Mul; //加入Mul方法
mathFunc(4,5);
blank();
Console.WriteLine("之后加Div方法:");
mathFunc += Div; //加入Div方法
mathFunc(20,4);
blank();
Console.WriteLine("delegate为闭包,可以抓取上下文信息");
int Val = 10;
Action CloSure = () =>
{
Console.WriteLine($"闭包抓取的上下文信息Val={Val}");
Val+= 5;
};
CloSure(); //调用闭包
Console.WriteLine("CloSure 一次后的Val值为:"+ Val);
}
delegate float MathFunc(float x, float y); //定义一个委托类型
static float Add(float x, float y)
{
Console.WriteLine($"{x}+{y}={x+y}");
return x+y;
}
static float Sub(float x, float y)
{
Console.WriteLine($"{x}-{y}={x-y}");
return x-y;
}
static float Mul(float x, float y)
{
Console.WriteLine($"{x}*{y}={x*y}");
return x * y;
}
static float Div(float x, float y)
{
Console.WriteLine($"{x}/{y}={x/y}");
return x / y;
}
static void blank() //空格
{
Console.WriteLine();
}
}
}这里是结果:
委托Func可以有返回值:
3+4=7
7
初始化Add:
10+5=15
之后加Sub方法:
2+3=5
2-3=-1
之后减Sub方法:
25+3.2=28.2
之后加Mul方法:
4+5=9
4*5=20
之后加Div方法:
20+4=24
20*4=80
20/4=5
delegate为闭包,可以抓取上下文信息
闭包抓取的上下文信息Val=10
CloSure 一次后的Val值为:15
可算是完事了