C#高级语法学习笔记自用
1.特性
要形象地理解C# Attribute(特性),我们可以将其想象成一种“标签”或“注解”,它们被“贴”在C#代码的不同元素(如类、方法、属性等)上,以提供额外的信息或元数据。这些元数据在编译时或运行时可以被编译器、运行时环境或其他工具读取和使用。
想象你有一个书架,书架上摆放着很多书(类)。每本书(类)都有自己的标题、作者和出版日期等基本信息(类的成员变量)。但是,除了这些信息之外,你可能还想给每本书添加一些额外的“标签”,比如“推荐读物”、“畅销书”、“历史类”等。这些标签就是书的特性(Attribute),它们为书提供了额外的分类或描述信息。
在C#中,你可以定义自己的特性类,就像你设计不同的书签一样。然后,你可以将这些特性类应用到你的代码元素上,就像你在书的封面或内页上贴上书签一样。例如,你可以定义一个[Serializable]特性,将其应用到需要序列化的类上,以告诉编译器这个类应该被序列化。或者,你可以定义一个自定义的特性,如[TestMethod],将其应用到需要被测试的方法上,以标记这个方法是一个测试方法。
这些特性在编译时或运行时会被编译器或运行时环境读取和使用。编译器可能会根据特性来生成额外的代码或执行特定的编译优化。运行时环境可能会根据特性来加载或配置对象。其他工具,如单元测试框架或代码生成器,也可以利用这些特性来执行特定的任务。
因此,C# Attribute就像是一种强大的元数据机制,它允许你为代码元素添加额外的信息或标签,并在编译时或运行时利用这些信息来执行特定的任务。通过这种方式,你可以更加灵活和高效地管理和控制你的代码
unity 相关
在Unity中,C# Attribute(特性)具有广泛的应用,它们为Unity引擎提供了额外的元数据信息,帮助开发者更有效地管理和控制游戏对象、组件和脚本的行为。以下是在Unity中C# Attribute的一些常见应用:
序列化与反序列化
[SerializeField]:允许在Unity的Inspector面板中显示私有字段,从而允许开发者在编辑器中直接设置和修改这些字段的值。
[NonSerialized]:与[SerializeField]相反,用于标记一个字段不应该被序列化。这在需要某些字段在运行时动态计算或不被编辑器持久化时很有用。
组件添加与管理
[RequireComponent]:在类上使用时,自动将所需的组件添加到使用该类的GameObject上。这有助于确保GameObject始终具有必要的组件。
[AddComponentMenu]:允许开发者在Unity编辑器中的Component菜单中添加自定义的组件选项。
自定义编辑器功能
[ContextMenu] 和 [ContextMenuItem]:在Inspector面板的ContextMenu中添加自定义选项,允许开发者通过右键菜单快速执行某些操作或调用方法。
[Header]:在Inspector面板中为字段组添加标题,有助于组织和管理大量字段。
[Space]:在Inspector面板中的字段之间添加额外的间距,提高可读性。
脚本执行与调试
[PostProcessBuild]:在构建项目后执行自定义的后期处理步骤,如自动修改文件、生成资源等。
[ExecuteInEditMode]:允许MonoBehaviour在Unity编辑器中不播放游戏时执行其Update方法,这在需要实时预览或调试时非常有用。
自定义标签与属性
开发者可以创建自定义的Attribute类,以提供对Unity项目中的类、方法、字段等的自定义元数据。这些自定义Attribute可以用于自动化脚本编写、代码生成、文档生成等任务。
网络编程与序列化
Unity的网络编程经常需要处理对象的序列化和反序列化。通过使用C# Attribute,可以轻松地标记哪些字段需要被序列化或反序列化,从而简化网络编程的复杂性。
Unity事件系统
在Unity的事件系统中,C# Attribute也发挥着重要作用。例如,通过使用[EventTrigger] Attribute,开发者可以在Unity UI元素上绑定自定义的事件处理方法。
总之,C# Attribute在Unity中的应用非常广泛,它们为开发者提供了强大的工具来管理和控制游戏对象、组件和脚本的行为。通过合理使用这些Attribute,开发者可以更加高效、灵活地开发游戏和其他Unity应用程序。
2.反射
我觉得这个就讲的很好
https://blog.youkuaiyun.com/qq_32175379/article/details/113880100
using System;
using System.Reflection;
using System.Threading;
namespace Lesson20_反射
{
#region 知识点回顾
//编译器是一种翻译程序
//它用于将源语言程序翻译为目标语言程序
//源语言程序:某种程序设计语言写成的,比如C#、C、C++、Java等语言写的程序
//目标语言程序:二进制数表示的伪机器代码写的程序
#endregion
#region 知识点一 什么是程序集
//程序集是经由编译器编译得到的,供进一步编译执行的那个中间产物
//在WINDOWS系统中,它一般表现为后缀为·dll(库文件)或者是·exe(可执行文件)的格式
//说人话:
//程序集就是我们写的一个代码集合,我们现在写的所有代码
//最终都会被编译器翻译为一个程序集供别人使用
//比如一个代码库文件(dll)或者一个可执行文件(exe)
#endregion
#region 知识点二 元数据
//元数据就是用来描述数据的数据
//这个概念不仅仅用于程序上,在别的领域也有元数据
//说人话:
//程序中的类,类中的函数、变量等等信息就是 程序的 元数据
//有关程序以及类型的数据被称为 元数据,它们保存在程序集中
#endregion
#region 知识点三 反射的概念
//程序正在运行时,可以查看其它程序集或者自身的元数据。
//一个运行的程序查看本身或者其它程序的元数据的行为就叫做反射
//说人话:
//在程序运行时,通过反射可以得到其它程序集或者自己程序集代码的各种信息
//类,函数,变量,对象等等,实例化它们,执行它们,操作它们
#endregion
#region 知识点四 反射的作用
//因为反射可以在程序编译后获得信息,所以它提高了程序的拓展性和灵活性
//1.程序运行时得到所有元数据,包括元数据的特性
//2.程序运行时,实例化对象,操作对象
//3.程序运行时创建新对象,用这些对象执行任务
#endregion
class Test
{
private int i = 1;
public int j = 0;
public string str = "123";
public Test()
{
}
public Test(int i)
{
this.i = i;
}
public Test( int i, string str ):this(i)
{
this.str = str;
}
public void Speak()
{
Console.WriteLine(i);
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("反射");
#region 知识点五 语法相关
#region Type
//Type(类的信息类)
//它是反射功能的基础!
//它是访问元数据的主要方式。
//使用 Type 的成员获取有关类型声明的信息
//有关类型的成员(如构造函数、方法、字段、属性和类的事件)
#region 获取Type
//1.万物之父object中的 GetType()可以获取对象的Type
int a = 42;
Type type = a.GetType();
Console.WriteLine(type);
//2.通过typeof关键字 传入类名 也可以得到对象的Type
Type type2 = typeof(int);
Console.WriteLine(type2);
//3.通过类的名字 也可以获取类型
// 注意 类名必须包含命名空间 不然找不到,System就是Int32的命名空间
Type type3 = Type.GetType("System.Int32");
Console.WriteLine(type3);
#endregion
#region 得到类的程序集信息
//可以通过Type可以得到类型所在程序集信息,会输出所在的程序集,版本
Console.WriteLine(type.Assembly);
Console.WriteLine(type2.Assembly);
Console.WriteLine(type3.Assembly);
#endregion
#region 获取类中的所有公共成员
//首先得到Type
Type t = typeof(Test);
//然后得到所有公共成员
//需要引用命名空间 using System.Reflection;
MemberInfo[] infos = t.GetMembers();
for (int i = 0; i < infos.Length; i++)
{
Console.WriteLine(infos[i]);
}
#endregion
#region 获取类的公共构造函数并调用
//1.获取所有构造函数
ConstructorInfo[] ctors = t.GetConstructors();
for (int i = 0; i < ctors.Length; i++)
{
Console.WriteLine(ctors[i]);
}
//2.获取其中一个构造函数 并执行
//得构造函数传入 Type数组 数组中内容按顺序是参数类型
//执行构造函数传入 object数组 表示按顺序传入的参数
// 2-1得到无参构造
ConstructorInfo info = t.GetConstructor(new Type[0]);
//执行无参构造 无参构造 没有参数 传null
Test obj = info.Invoke(null) as Test;
Console.WriteLine(obj.j);
// 2-2得到有参构造
ConstructorInfo info2 = t.GetConstructor(new Type[] { typeof(int) });
obj = info2.Invoke(new object[] { 2 }) as Test;
Console.WriteLine(obj.str);
ConstructorInfo info3 = t.GetConstructor(new Type[] { typeof(int), typeof(string) });
obj = info3.Invoke(new object[] { 4, "444444" }) as Test;
Console.WriteLine(obj.str);
#endregion
#region 获取类的公共成员变量
//1.得到所有成员变量
FieldInfo[] fieldInfos = t.GetFields();
for (int i = 0; i < fieldInfos.Length; i++)
{
Console.WriteLine(fieldInfos[i]);
}
//2.得到指定名称的公共成员变量,这里只是获取到类对象,而不是值
FieldInfo infoJ = t.GetField("j");
Console.WriteLine(infoJ);
//3.通过反射获取和设置对象的值
Test test = new Test();
test.j = 99;
test.str = "2222";
// 3-1通过反射 获取对象的某个变量的值
Console.WriteLine(infoJ.GetValue(test));
// 3-2通过反射 设置指定对象的某个变量的值,就是给test对象的j,设置100,
infoJ.SetValue(test, 100);
Console.WriteLine(infoJ.GetValue(test));
#endregion
#region 获取类的公共成员方法
//通过Type类中的 GetMethod方法 得到类中的方法
//MethodInfo 是方法的反射信息
Type strType = typeof(string);
MethodInfo[] methods = strType.GetMethods();//获取String类中的所有的公共方法
for (int i = 0; i < methods.Length; i++)
{
Console.WriteLine(methods[i]);
}
//1.如果存在方法重载 用Type数组表示参数类型
MethodInfo subStr = strType.GetMethod("Substring",
new Type[] { typeof(int), typeof(int) });
//2.调用该方法
//注意:如果是静态方法 Invoke中的第一个参数传null即可
string str = "Hello,World!";
//第一个参数 相当于 是哪个对象要执行这个成员方法
object result = subStr.Invoke(str, new object[] { 7, 5 });
Console.WriteLine(result);
#endregion
#region 其它
//Type;
//得枚举
//GetEnumName
//GetEnumNames
//得事件
//GetEvent
//GetEvents
//得接口
//GetInterface
//GetInterfaces
//得属性
//GetProperty
//GetPropertys
//等等
#endregion
#endregion
#region Assembly
//程序集类
//主要用来加载其它程序集,加载后
//才能用Type来使用其它程序集中的信息
//如果想要使用不是自己程序集中的内容 需要先加载程序集
//比如 dll文件(库文件)
//简单的把库文件看成一种代码仓库,它提供给使用者一些可以直接拿来用的变量、函数或类
//三种加载程序集的函数
//一般用来加载在同一文件下的其它程序集
//Assembly asembly2 = Assembly.Load("程序集名称");
//一般用来加载不在同一文件下的其它程序集
//Assembly asembly = Assembly.LoadFrom("包含程序集清单的文件的名称或路径");
//Assembly asembly3 = Assembly.LoadFile("要加载的文件的完全限定路径");
//1.先加载一个指定程序集
Assembly asembly = Assembly.LoadFrom(@"C:\Users\MECHREVO\Desktop\CSharp进阶教学\Lesson18_练习题\bin\Debug\netcoreapp3.1\Lesson18_练习题");
Type[] types = asembly.GetTypes();//得到所有的类
for (int i = 0; i < types.Length; i++)
{
Console.WriteLine(types[i]);
}
//得到上面所有的类的名称后,在根据类的名称去创建对象
//2.再加载程序集中的一个类对象 之后才能使用反射
Type icon = asembly.GetType("Lesson18_练习题.Icon");
MemberInfo[] members = icon.GetMembers();//获取所有的类的成员变量
for (int i = 0; i < members.Length; i++)
{
Console.WriteLine(members[i]);
}
//通过反射 实例化一个 icon对象
//首先得到枚举Type 来得到可以传入的参数
//枚举也算个类,
Type moveDir = asembly.GetType("Lesson18_练习题.E_MoveDir");
FieldInfo right = moveDir.GetField("Right");//得到类对象
//直接实例化对象
object iconObj = Activator.CreateInstance(icon, 10, 5, right.GetValue(null));
//得到对象中的方法 通过反射
MethodInfo move = icon.GetMethod("Move");
MethodInfo draw = icon.GetMethod("Draw");
MethodInfo clear = icon.GetMethod("Clear");
Console.Clear();
while(true)
{
Thread.Sleep(1000);
clear.Invoke(iconObj, null);
move.Invoke(iconObj, null);
draw.Invoke(iconObj, null);
}
//3.类库工程创建
#endregion
#region Activator
//用于快速实例化对象的类
//用于将Type对象快捷实例化为对象
//先得到Type
//然后 快速实例化一个对象
Type testType = typeof(Test);
//1.无参构造
Test testObj = Activator.CreateInstance(testType) as Test;
Console.WriteLine(testObj.str);
//2.有参数构造
testObj = Activator.CreateInstance(testType, 99) as Test;
Console.WriteLine(testObj.j);
testObj = Activator.CreateInstance(testType, 55, "111222") as Test;
Console.WriteLine(testObj.j);
#endregion
#endregion
}
}
//总结
//反射
//在程序运行时,通过反射可以得到其他程序集或者自己的程序集代码的各种信息
//类、函数、变量、对象等等,实例化他们,执行他们,操作他们
//关键类
//Type
//Assembly
//Activator
//对于我们的意义
//在初中级阶段 基本不会使用反射
//所以目前对于大家来说,了解反射可以做什么就行
//很长时间内都不会用到反射相关知识点
//为什么要学反射
//为了之后学习Unity引擎的基本工作原理做铺垫
//Unity引起的基本工作机制 就是建立在反射的基础上
}
3.索引器
https://blog.youkuaiyun.com/qq_45997271/article/details/131803979
4.委托
个人感觉这些不错
https://www.runoob.com/csharp/csharp-delegate.html
https://www.bilibili.com/video/BV1ai4y1K7Pk/?spm_id_from=333.788.recommend_more_video.0&vd_source=db053bd64f80538bff88a3baa7481c37 ·
5.事件
由来
事件本质上就是一种特殊的委托,既然有了委托,为什么还需要事件?
在C#中,尽管委托(delegate)本身已经足够强大,可以作为一种类型安全的函数指针来传递方法引用,但事件(event)仍然被引入作为一种特殊的成员类型,主要有以下几个原因:
封装性:
事件提供了一种封装委托的方式,使得类的内部状态改变时能够通知外部的观察者(订阅者),但同时又不会暴露内部实现的细节。通过事件,类的实现者可以控制哪些方法可以被外部订阅,以及何时触发这些订阅的方法。
访问控制:
事件通常被声明为public,但事件的内部委托成员可以是private的。这意味着外部代码可以订阅或取消订阅事件,但无法直接调用或修改事件内部的委托。这增加了代码的安全性,防止了外部代码误操作或滥用内部委托。
发布-订阅模式:
事件是实现发布-订阅模式(Publisher-Subscriber Pattern)的一种自然方式。在这种模式中,发布者(Publisher)发布事件,而订阅者(Subscriber)可以订阅这些事件以便在事件发生时接收通知。事件提供了一种解耦的机制,使得发布者和订阅者之间不需要直接依赖或了解彼此的实现细节。
多线程安全性:
虽然委托本身并不直接提供多线程安全性,但事件在C#中的实现通常考虑了这一点。当事件在多线程环境中触发时,事件发布者不需要担心订阅者方法的执行顺序或线程安全性问题,因为事件的触发机制会负责正确地调用所有订阅者方法。
类型安全:
尽管委托本身已经提供了类型安全性,但事件通过限制可以订阅的方法类型(即委托类型)进一步增强了类型安全性。这确保了只有符合特定签名的方法才能被订阅到事件上,从而减少了运行时错误的可能性。
可维护性和可读性:
通过显式地声明和使用事件,代码的可维护性和可读性通常会有所提高。事件提供了一种清晰的方式来表示类的哪些状态改变是外部可观察的,以及这些状态改变应该如何通知外部代码。这有助于其他开发者更好地理解类的行为和如何与其交互。
综上所述,尽管委托已经足够强大,但事件作为一种特殊的成员类型,提供了额外的封装性、访问控制、发布-订阅模式支持、多线程安全性、类型安全性以及可维护性和可读性等方面的优势。因此,在C#中同时使用委托和事件是一种常见的做法。
如何理解
在C#中,事件(Event)可以被形象地解释为一种“通知机制”,它允许一个对象(发布者)在发生特定事情时通知其他对象(订阅者)。这种机制让代码更加模块化、解耦,并且易于扩展和维护。
想象一下,你正在组织一个派对,并且你希望当某些特定的活动(比如食物准备好了、音乐开始了)发生时,能够通知到所有的客人(订阅者)。在这种情况下,你可以把你自己看作是“发布者”,而每个客人都是“订阅者”。
事件就是你为了通知客人而设置的一个机制:
**定义事件:**首先,你需要定义一个事件,这就像是你在派对上挂一个公告板,上面写着“当食物准备好时,请来这里”。这个公告板就是事件,它告诉所有人:“在某个特定的时刻,我会发布一个通知”。
事件类型:事件通常与一个特定的类型(委托)相关联,这个类型定义了哪些人可以响应这个事件(即哪些方法可以被调用)。在这个例子中,你的公告板可能只接受那些对食物感兴趣的人(方法),他们可能是厨师、服务员或者饥饿的客人。
订阅事件:客人们看到公告板后,如果他们对食物感兴趣,就会把自己的名字(方法引用)写在公告板上,表示他们愿意在食物准备好时收到通知。这个过程就是“订阅事件”。
触发事件:当食物真的准备好了,你就会查看公告板,找到所有对食物感兴趣的人的名字,然后逐个通知他们。这个过程就是“触发事件”。在C#中,这通常是通过调用委托的Invoke方法或者使用委托的调用列表语法(delegate?())来实现的。
取消订阅:有些客人可能在派对过程中离开了,或者不再对食物感兴趣了。他们可以从公告板上擦掉自己的名字,这样他们就不会再收到通知了。这就是“取消订阅事件”。
通过这种机制,你可以很容易地控制哪些人(方法)会收到通知,以及在什么时候收到通知。同时,你也不需要知道具体的客人是谁(即不需要知道订阅了事件的具体对象和方法是什么),只需要按照公告板上的名单进行通知即可。这大大增加了代码的灵活性和可维护性。
在C#中,事件是通过event关键字来定义的,并且通常与委托一起使用。委托定义了事件的类型(即哪些方法可以被订阅),而事件本身则提供了订阅、触发和取消订阅的机制。
例子
下面是一个**Unity(并没有使用内置Aciton 、Fun)**事件使用的基本例子,我们将通过创建一个简单的金币拾取事件来展示如何定义和使用事件。
第一步 定义事件
首先,我们需要定义一个包含事件的类,用于在金币数量发生变化时触发事件。
using UnityEngine;
public class GoldEvents
{
// 定义一个事件,当金币增加时触发
private delegate void OnGoldChange(int goldAmount);
public event OnGoldChange onGoldGainedEvent;
//Action 、Fun 是unity内置的委托,前者没有返回值,后者有一个返回值;都具有16个重载方法
//private delegate void OnGoldChange(int goldAmount);
// public event OnGoldChange onGoldGainedEvent;
//上面的二行,用unity 内置的委托的话,可以写作 public event Aciton<int> onGoldGainedEvent;
// 调用事件的方法
public void NotifyGoldGained(int goldAmount)
{
onGoldGained?.Invoke(goldAmount);
}
}
第二步 订阅事件
然后,我们创建一个玩家类(Player),这个类将订阅GoldEvents类中的onGoldGainedEvent事件。
using UnityEngine;
public class Player : MonoBehaviour
{
// 假设Player有一个GoldEvents的实例
private GoldEvents goldEvents;
void Start() //**unity 内置生命周期方法,简单理解为Main方法 ,只执行一次**
{
// 初始化GoldEvents实例
goldEvents = new GoldEvents();
// 订阅事件
goldEvents.onGoldGainedEvent += HandleGoldGained;
}
// 处理金币增加事件的方法
private void HandleGoldGained(int goldAmount)
{
Debug.Log("玩家获得了 " + goldAmount + " 个金币!");
}
// OnDestroy 是一个特殊的生命周期方法,它在MonoBehaviour对象即将被销毁时自动被调用
//在不需要时取消订阅事件,避免内存泄漏
void OnDestroy()
{
goldEvents.onGoldGainedEvent -= HandleGoldGained;
}
}
第三步 触发事件
最后,当金币数量发生变化时,我们需要触发GoldEvents类中的onGoldGained事件。
这可以在其他类的某个方法中进行,例如一个处理金币拾取的类:
public class GoldPickupHandler : MonoBehaviour
{
// 假设这里有一个GoldEvents的实例
private GoldEvents goldEvents;
// 假设这是处理金币拾取的方法
public void PickupGold(int goldAmount)
{
// 触发事件,通知所有订阅者金币增加了
goldEvents.NotifyGoldGained(goldAmount);
}
}
在这个例子中,当GoldPickupHandler的PickupGold方法被调用时,它会通过GoldEvents的NotifyGoldGained方法来触发onGoldGained事件,从而通知所有订阅了该事件的对象(如Player类)。Player类中的HandleGoldGained方法将被调用,并在控制台输出玩家获得的金币数量。
6.集合
在C#中,集合(Collections)是存储数据的容器,它们提供了对一组对象进行存储、检索、遍历、添加和删除等操作的功能。C#标准库提供了多种集合类型,每种类型都有其特定的用途和性能特性。以下是一些常用的C#集合类型及其介绍:
(1)Array(数组):
数组是最简单的集合类型,它用于存储固定数量的相同类型的元素。
一旦数组被创建,其大小就不能改变。
数组可以是一维、二维或多维的。
使用 [] 索引器访问数组元素。
(1)ArrayList(已过时,建议使用List):
ArrayList 是一个可以动态调整大小的数组,用于存储任何类型的对象(因为实际存储的是 object 类型的元素,所以会有装箱和拆箱的开销)。
由于性能原因和类型安全性的考虑,现在通常推荐使用 List。
(2)List:
List 是一个泛型集合,用于存储同一类型的元素。
它提供了添加、插入、删除、搜索和遍历元素的方法。
由于是泛型,所以不存在装箱和拆箱的开销。
常用的方法包括 Add(), Remove(), Contains(), IndexOf(), Count 等。
(3)Dictionary<TKey, TValue>:
Dictionary<TKey, TValue> 是一个基于哈希表的集合,用于存储键值对。
键(Key)必须是唯一的,而值(Value)可以重复。
它提供了通过键快速查找、添加和删除元素的功能。
常用的方法包括 Add(), Remove(), ContainsKey(), TryGetValue(), Clear() 等。
(4)HashSet < T >:
HashSet 是一个不包含重复元素的集合。
它提供了添加、删除、检查元素是否存在的高效操作。
由于基于哈希表实现,所以查找操作的时间复杂度接近O(1)。
(5)Queue< T >:
Queue 是一个先进先出(FIFO)的集合,用于存储需要按顺序处理的元素。
它提供了入队(Enqueue)和出队(Dequeue)操作。
(6)Stack< T >:
Stack 是一个后进先出(LIFO)的集合,用于存储需要按相反顺序处理的元素。
它提供了压栈(Push)和弹栈(Pop)操作。
(7)LinkedList:
LinkedList 是一个双向链表,用于存储元素并提供在链表中的任意位置进行插入和删除操作的功能。
它提供了从头和尾添加、删除元素的方法,以及遍历整个链表的方法。
(8)SortedDictionary<TKey, TValue> 和 SortedList<TKey, TValue>:
这两个集合类型都用于存储键值对,并且键在集合中是按照升序排序的。
SortedDictionary<TKey, TValue> 通常具有更好的性能,尤其是在元素数量很大的情况下。
(9)IEnumerable< T > 和 IEnumerator< T >:
这两个接口是所有集合类型的基础,用于支持迭代和遍历操作。
你可以通过 foreach 循环来遍历实现了 IEnumerable 接口的集合。
这些集合类型在C#编程中非常常用,根据具体的需求和场景选择合适的集合类型可以提高代码的性能和可读性。
7.泛型(重学版)
泛型的概念很简单,要是不理解的话,遇到什么泛型委托接口,什么就会很懵逼。
所谓泛型,个人理解就得拆开理解 ,'泛’指的是广泛,通用,'型’指的是类型,无论是语言自带还是我们自定义的,
由来
泛型的出现主要是为了解决编程中遇到的一些常见问题,特别是与类型相关的问题。以下是泛型出现的主要原因和背景,结合了参考文章中的相关数字和信息进行解释:
代码重用和灵活性:
在没有泛型的时代,如果需要处理不同类型的数据,往往需要编写多个功能相似但类型不同的方法或类。这导致了代码冗余和不可维护性。
泛型允许程序员编写与类型无关的通用代码,这些代码可以在运行时与各种类型的数据一起使用。这大大提高了代码的重用性和灵活性。
类型安全:
在没有泛型之前,如果需要在集合中存储多种类型的数据,通常会使用Object作为集合元素的类型。这导致在取出元素时需要进行显式的类型转换,而这种转换是危险的,因为如果在运行时元素的类型与期望的类型不匹配,将会抛出ClassCastException。
泛型的引入允许在定义集合时就指定元素的类型,从而在编译时就能进行类型检查,避免了运行时的类型转换错误,提高了程序的类型安全性。
历史发展:
泛型的历史可以追溯到1946年,美国数学家阿尔茨·图灵(Alan Turing)提出了“可计算性理论”中的“可变参数函数”概念,这成为泛型的基础。
泛型编程的理论最早由Dave Musser在1971年提出并推广,但主要局限于软件开发和计算机代数领域。
1979年,Alexander Stepanov开始研究泛型编程,并认识到了其巨大潜力。他提出了STL(Standard Template Library)的体系结构,为泛型编程的广泛应用奠定了基础。
1993年,C++在3.0版中引入了模版技术,这是泛型编程在主流编程语言中的首次实现。
2004年,Java在J2SE 5.0(JDK 1.5)中引入了泛型技术,进一步推动了泛型编程的普及。
提高性能:
在没有泛型的情况下,为了处理多种类型的数据,往往需要进行显式的类型转换或装箱/拆箱操作,这会影响程序的性能。
泛型允许在编译时确定数据的类型,从而避免了运行时的类型转换和装箱/拆箱操作,提高了程序的执行效率。
综上所述,泛型的出现主要是为了解决代码重用、类型安全和性能等问题。通过引入类型参数的概念,泛型允许程序员编写更加灵活、类型安全和高效的代码。
8.匿名方法
C# 引入匿名方法的主要原因是为了简化委托的实现和调用,方便在委托调用的方法中处理那些只被调用一次或临时使用的简短代码块。匿名方法的出现使得程序员在不需要为某个特定任务定义一个完整的具名方法时,能够直接在需要的地方定义并调用一个简短的代码块,从而减少了编码系统开销。
匿名方法的好处
**简化代码:**匿名方法使得代码更加简洁,避免了为临时或简短的代码块定义完整的具名方法。
减少系统开销:由于匿名方法只在定义时调用,因此减少了实例化委托所需的编码系统开销。
易于理解:对于只在一个地方使用的简短代码块,使用匿名方法可以使代码更加易于理解。
与Lambda表达式协同:C# 3.0后,Lambda表达式的出现使得匿名方法的用途更加广泛,两者结合使用可以进一步简化代码。
匿名方法的坏处
**1.难以维护:**由于匿名方法没有名字,因此在代码中搜索和定位它们可能会比较困难,增加了维护的难度。
可能导致内存泄露:如果使用匿名方法订阅事件,并且频繁使用或忘记取消订阅,可能会导致严重的资源泄露甚至内存崩溃。这是因为匿名函数本身没有引用的对象,因此取消订阅或取消引用非常困难。
可读性:在某些情况下,使用匿名方法可能会降低代码的可读性,特别是对于不熟悉匿名方法使用方式的程序员来说。
2.调试困难:匿名方法在调试时可能会带来一些困难,因为它们没有明确的名称和位置,使得在调试过程中难以跟踪和定位问题。
总结
匿名方法在C#中是一个有用的特性,它允许程序员在需要的地方定义并调用简短的代码块,从而简化了代码的编写和调用过程。然而,匿名方法也存在一些潜在的问题,如难以维护、可能导致内存泄露、可读性和调试困难等。因此,在使用匿名方法时需要权衡其利弊,根据具体的需求和场景来决定是否使用它们。
菜鸟教程关于匿名方法连接,点击即到达
9.不安全代码
这玩意不常用
C# 语法中的“不安全代码”(Unsafe Code)是一个特殊的概念,它允许开发者直接操作内存地址,使用指针等低级别的数据结构。这种代码在通常的安全代码(Safe Code)中是不被允许的,但在某些特定的应用场景下(如性能敏感的任务或者与操作系统的底层交互)却非常有用。
以下是关于C#不安全代码的详细介绍:
- 定义与用途
定义:不安全代码是指那些直接操作内存地址的代码,它允许开发者使用指针等低级别的数据结构。
用途:不安全代码在性能敏感的任务或者需要与操作系统底层进行交互的场景下特别有用。例如,当需要直接操作硬件、调用非托管代码或进行底层优化时,不安全代码可以提供必要的灵活性。 - 特性与语法
unsafe 修饰符:在方法、属性或类的声明中使用 unsafe 修饰符,可以表示该方法、属性或类包含不安全代码。
指针类型:在不安全的上下文中,可以使用指针类型(如 int*、float* 等)来声明和操作指针。
内存操作:不安全代码允许开发者使用指针直接访问和修改内存中的数据。 - 使用方法与注意事项
使用方法:
在方法、属性或类的声明前添加 unsafe 修饰符。
在不安全代码块中使用指针进行内存操作。
注意事项:
使用不安全代码时要特别小心,因为直接操作内存可能导致程序崩溃、数据损坏或安全漏洞。
必须确保对指针的操作是正确和安全的,避免越界访问、空指针引用等错误。
在使用不安全代码时,应尽量减少其使用范围,避免对整个程序造成不必要的风险。 - 编译与运行
编译:为了编译包含不安全代码的程序,需要使用支持不安全代码的编译器选项(如 /unsafe)。
运行:在不支持不安全代码的运行环境中,包含不安全代码的程序可能无法正常运行。因此,在发布程序之前,应确保目标运行环境支持不安全代码。 - 示例
以下是一个简单的C#不安全代码示例,展示了如何使用指针访问和修改整数值:
using System;
class Program
{
static unsafe void Main()
{
int num = 25;
int* ptr = # // 获取num的地址并赋值给ptr
Console.WriteLine("原始值: {0}", num);
*ptr = 50; // 通过指针修改num的值
Console.WriteLine("修改后的值: {0}", num);
}
}
在这个示例中,我们首先声明了一个整数变量 num 并为其分配了一个值。然后,我们使用 unsafe 修饰符声明了一个 Main 方法,并在该方法中声明了一个指向整数的指针 ptr。我们通过 &num 获取 num 的地址并将其赋值给 ptr。接着,我们通过解引用指针 *ptr 修改了 num 的值。最后,我们打印出修改后的 num 的值
点击 跳转到 菜鸟不安全代码
10.多线程
C# 中的多线程是一个强大的编程概念,它允许一个应用程序同时执行多个任务或线程。为了更形象地解释多线程,我们可以使用日常生活中的一些例子。
想象你是一家餐厅的经理,你的餐厅有很多客人需要服务。每个客人都是一个独立的任务,包括点餐、准备食物、送餐和收款等步骤。
单线程(无多线程):
如果没有多线程,你就只能一次服务一个客人。你必须完成一个客人的所有步骤(点餐、准备食物、送餐、收款),然后才能开始为下一个客人服务。这会导致客人在等待上菜时无事可做,甚至可能会因为等待时间过长而离开。
多线程:
使用多线程,你可以同时服务多个客人。你可以让一个服务员(线程)去为第一个客人点餐,同时让另一个服务员(另一个线程)去为第二个客人准备食物。这样,即使第一个客人的食物还没有准备好,第二个客人也可以开始他的用餐体验。
多线程就像你的餐厅有很多服务员一样,他们可以并行工作,大大提高了餐厅的效率和客户满意度。
在 C# 中,多线程编程也是类似的。你可以创建多个线程来并行执行不同的任务,从而提高应用程序的响应性和性能。然而,多线程编程也带来了一些挑战,如线程同步、数据共享和死锁等问题,需要仔细管理和设计以避免潜在的问题。
为了管理多线程,C# 提供了多种同步机制,如锁(lock)、监视器(Monitor)、互斥体(Mutex)、信号量(Semaphore)、事件(Event)和异步编程模型(如 Task Parallel Library 和 async/await)等。这些机制可以帮助你确保线程之间的正确交互和数据一致性。
协程(关于Unity方向)
协程不是真正的多线程,但是可以模拟多线程
在Unity中,协程(Coroutines)是一种特殊的函数,它们允许你在单线程环境中编写类似多线程的代码。协程允许你分段执行代码,并在每段之间挂起和恢复执行,而不需要使用多线程的复杂性。Unity的协程通常在MonoBehaviour类的派生类中使用,如GameObject的脚本组件。
协程的特点:
单线程:协程在Unity的主线程上运行,这意味着它们不会创建额外的线程。
分段执行:协程可以在执行过程中被挂起,并在稍后恢复执行。
使用简单:相比多线程编程,协程的使用更为简单,不需要处理线程同步和通信等复杂问题。
协程的创建与调用:
在Unity中,你可以使用StartCoroutine方法来启动一个协程。该方法接受一个返回IEnumerator类型的函数作为参数。你可以在该函数中使用yield return语句来挂起协程的执行,并在满足某个条件时恢复执行。
协程的简单例子:
下面是一个简单的Unity协程例子,它演示了如何使用协程来模拟一个倒计时效果:
using System.Collections;
using UnityEngine;
public class CountdownCoroutineExample : MonoBehaviour
{
public float countdownDuration = 5.0f; // 倒计时时长
void Start()
{
// 启动协程
StartCoroutine(Countdown());
}
IEnumerator Countdown()
{
float elapsedTime = 0.0f;
while (elapsedTime < countdownDuration)
{
// 更新倒计时剩余时间
float remainingTime = countdownDuration - elapsedTime;
Debug.Log("倒计时剩余时间: " + remainingTime.ToString("F2") + " 秒");
// 挂起协程一段时间(这里是每秒挂起一次)
yield return new WaitForSeconds(1.0f);
// 更新已过去的时间
elapsedTime += 1.0f;
}
// 倒计时结束,输出提示信息
Debug.Log("倒计时结束!");
// 协程执行完毕
yield break;
}
}
在这个例子中,我们创建了一个名为CountdownCoroutineExample的脚本组件,它继承自MonoBehaviour。在Start方法中,我们调用了StartCoroutine方法来启动一个名为Countdown的协程。Countdown方法是一个返回IEnumerator类型的函数,它使用了一个while循环来模拟倒计时效果。在每次循环中,我们使用Debug.Log来输出倒计时剩余时间,并使用yield return new WaitForSeconds(1.0f);来挂起协程一秒钟。当倒计时结束时,我们输出一条提示信息,并使用yield break;来结束协程的执行。