前言

如果你有以上的基础,你就不用看,如果没有,你就去看

这里主要就是为了防止石山+怎么使用容器,以及超绝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

可算是完事了

这个是屑