前言
可算是入门结束了,真是累的要命啊,终于要开始写游戏了(当然,这是入门之后的事了)
总之如你们所见,就这么点内容,不过还是挺基础、重要的,毕竟还算是基本功
以上的内容虽然看着多,但实际上如果学过其他相关的语言(如java,py),你可以简单省略异常处理和重载这部分,看完换言之也就差不多完事了
反射
定义
本质上就是获取代码信息的:主要就看三种:一种是类型信息,一种是变量,一种是函数
虽然你们能知道有很多,比如程序集,但目前没必要
不过微软文档倒是回答挺多的(原链接:.NET 中的反射 | Microsoft Learn)
命名空间中的System.Reflection类以及System.Type可用于获取有关加载的程序集及其中定义的类型的信息,例如类、接口和值类型(即结构和枚举)。 还可以使用反射在运行时创建类型实例,以及调用和访问它们。
程序集 包含模块、模块包含类型和类型包含成员。 反射提供封装程序集、模块和类型的对象。 可以使用反射动态创建类型的实例、将类型绑定到现有对象或从现有对象获取类型。 然后,可以调用该类型的方法或访问其字段和属性。
用法
实际上这玩意还有很多类型,不过一般来说没人会深入写这种,主要是没太大必要,至少入门是这样的,因此我就简单过一遍,虽然微软官方有很多标准用法,以下便是其用法(原链接:.NET 中的反射 | Microsoft Learn):
用于
Assembly定义和加载程序集、加载程序集清单中列出的模块,并从此程序集中找到类型并创建它的实例。使用
Module可发现包含模块的程序集以及模块中的类等信息。 还可以获取模块上定义的所有全局方法或其他特定非全局方法。用
ConstructorInfo发现构造函数的相关信息,例如名称、参数、访问修饰符(如public和private)以及实现详细信息(如abstract和virtual)等。 使用GetConstructors的GetConstructor或Type方法来调用特定构造函数。用于
MethodInfo发现方法的名称、返回类型、参数、访问修饰符和实现详细信息(如abstract或virtual)等信息。 使用GetMethods中的GetMethod或Type方法调用特定方法。使用
FieldInfo以发现字段的名称、访问修饰符和static等实现详细信息,以及获取或设置字段值。使用
EventInfo来发现事件的名称、事件处理程序的数据类型、自定义属性、声明类型和反射的类型等信息,以及添加或删除事件处理程序。用于
PropertyInfo发现诸如名称、数据类型、声明类型、反射类型以及属性的只读或可写状态等信息,以及获取或设置属性值。用于
ParameterInfo发现参数的名称、数据类型、参数是输入参数还是输出参数以及参数在方法签名中的位置等信息。当你在
CustomAttributeData或仅反射上下文 (.NET Framework) 中工作时使用MetadataLoadContext发现有关自定义特性的信息。CustomAttributeData允许你检查属性,而无需创建它们的实例。
一般来说,以上的方法都会用到,但前俩种和最后一种一般来说不太会用到(入门时期),其他可以简单记记
示例
在这里我就简单演示其基础用法(什么程序集啥的一般也用不到),以便于理解反射是干什么的:
namespace Sixth
{
internal class Program
{
static void Main(string[] args)
{
int number = 42;
//反射示例(获取变量的类型信息)
Type type = number.GetType(); // 获取变量的类型信息
System.Console.WriteLine($"Type of number: {type}");
//反射示例(获取类型的类型信息)
Type type1 = typeof(int); // 获取int类型的类型信息
System.Console.WriteLine($"Type of int: {type1}");
//反射示例(获取类型的完整名称)
string typeName = type1.FullName; // 获取类型的完整名称
System.Console.WriteLine($"Full name of int type: {typeName}");
//反射示例(获取方法)
var methodInfo = type1.GetMethod("ToString", Type.EmptyTypes); // 获取ToString方法的信息
System.Console.WriteLine($"Method info of ToString: {methodInfo}");
//获取结果
string result = methodInfo.Invoke(number, null) as string; // 调用ToString方法
//string result = (string)methodInfo.Invoke(number, null); // 调用ToString方法(另一种写法)
System.Console.WriteLine($"Result of ToString method: {result}");
}
}
}这里是结果:
Type of number: System.Int32
Type of int: System.Int32
Full name of int type: System.Int32
Method info of ToString: System.String ToString()
Result of ToString method: 42
预处理器指令
定义
预处理器指令在C#中用于条件编译,它们不是宏(C#中根本就没有宏),这玩意与C/C++的宏不同,C#的预处理器指令不会进行文本替换,而是在编译时根据条件包含或排除代码块的,而预处理器指令这玩意就主要用来搞条件编译的,主要用于分离调试版本和发布版本的代码、编写平台特定代码或功能开关等
不过如果只是简单的入门下,拿去写算法题整点花活什么的,这玩意还真没啥太大用,如果说是工程文件上,那就会非常有用
用法
正如前文所提,他就是用来预处理的,不过我这里还是简单引用微软官方的话来讲(原链接:预处理器指令 - C# reference | Microsoft Learn):
尽管编译器没有单独的预处理器,但本节中所述指令的处理方式与有预处理器时一样。 可使用这些指令来帮助条件编译。 不同于 C 和 C++ 指令,不能使用这些指令来创建宏。 预处理器指令必须是一行中唯一的说明。
总之如果是入门的,只用了解,预处理器指令是根据条件进行编译的即可,具体可见微软文档(预处理器指令 - C# reference | Microsoft Learn)
示例
这里演示比较麻烦,不过由于没必要说一定得要写预处理器指令,我这里就写俩段类似的代码以便演示:
//预处理器指令示例
#define TEST //定义TEST预处理器指令
#if TEST //如果定义了TEST预处理器指令
#define DEBUG //定义DEBUG预处理器指令
#else //否则
#undef DEBUG //取消定义DEBUG预处理器指令
#endif //预处理器指令示例结束
namespace Sixth
{
internal class Program
{
static void Main(string[] args)
{
#if DEBUG //如果定义了DEBUG预处理器指令
System.Console.WriteLine("Debug version");
#else //否则
System.Console.WriteLine("Release version");
#endif //预处理器指令示例结束
}
}
}这里是结果:
Debug version
若注释TEST预定义符号,则为以下代码:
//预处理器指令示例
//#define TEST //定义TEST预处理器指令,这里注释掉以便演示
#if TEST //如果定义了TEST预处理器指令
#define DEBUG //定义DEBUG预处理器指令
#else //否则
#undef DEBUG //取消定义DEBUG预处理器指令
#endif //预处理器指令示例结束
namespace Sixth
{
internal class Program
{
static void Main(string[] args)
{
#if DEBUG //如果定义了DEBUG预处理器指令
System.Console.WriteLine("Debug version");
#else //否则
System.Console.WriteLine("Release version");
#endif //预处理器指令示例结束
}
}
}这里是结果:
Release version
差不多讲完预处理器指令了,就搞那个异常处理了
异常处理
定义
如果说写过其他语言的,那会对这玩意非常熟悉,但为了照顾没学过的,我这里简单解释下:
异常处理最简单的话来讲就是处理出错的(当然是运行时出现的错误),如果不处理,就会出现直接崩溃的发生(也就是程序强制结束)以及处理事情会很头疼(主要体现为可读性非常差、难以维护等),也就是维护性会变的非常差,因此异常处理是非常重要的(当然,得要合理使用,不要一上来就无脑用Exception,最好是先搞能遇到的异常,比如DivideByZeroException,再去思考其他异常)
用法
C#的异常处理其实和java的异常处理特别像(尽管某些用法还是有区别的),主要就两种,一种是throw(抛出异常),一种是try(捕获异常)
而在try(捕获异常)里,可分为三种,分别为:
try-catch:尝试执行-捕获异常
try-finally:无论是否异常,finally强制执行
try-catch-finally:结合2、3
示例
我这里展示俩种:
throw版:
namespace Sixth
{
internal class Program
{
static void Main(string[] args)
{
double number = double.Parse(Console.ReadLine()!); //读取第一个数字
double number2 = double.Parse(Console.ReadLine()!); //读取第二个数字
if (number2 == 0) //判断除数是否为零
{
throw new DivideByZeroException("除数不能为零");
}
Console.WriteLine($"{number} / {number2} = {number / number2}"); //输出结果(若判断除数不为零)
}
}
}这里就是通过throw故意创造一个异常的,如果输入0,则直接抛出
这里如果选择直接抛出会使程序崩溃,还得要处理异常
Catch版:
namespace Sixth
{
internal class Program
{
static void Main(string[] args)
{
try //尝试执行以下代码块
{
int number = int.Parse(Console.ReadLine()!); //输入一个整数
int number2 = int.Parse(Console.ReadLine()!); //输入另一个整数
Console.WriteLine($"{number} / {number2} = {number / number2}"); //输出A/B的结果
}
catch (DivideByZeroException e) //捕获除零异常
{
Console.WriteLine("除数不能为零"); //提示用户除数不能为零
Console.WriteLine(e.Message); //捕获除零异常
Console.WriteLine(e.ToString()); //输出异常的详细信息
}
catch (FormatException e) //捕获格式异常
{
Console.WriteLine("输入的格式不正确,请输入整数"); //提示用户输入格式不正确
Console.WriteLine(e.Message); //输出异常信息
Console.WriteLine(e.ToString()); //输出异常的详细信息
}
catch (Exception e) // 捕获剩下所有异常(应该放在最后)
{
Console.WriteLine($"发生未预期的错误: {e.Message}");
}
finally //无论是否发生异常,都会执行以下代码块
{
Console.WriteLine("程序执行结束"); //输出程序执行结束
}
}
}
}这里简单做个演示:
输入正常的内容:
15
2
输出:
15 / 2 = 7
程序执行结束
以上就是正常的,接下来为了验证错误内容,我这里就输入错误的内容的情况(仅演示0/0的情况):
0
0
输出:
除数不能为零
Attempted to divide by zero.
System.DivideByZeroException: Attempted to divide by zero.
at Sixth.Program.Main(String[] args) in C:\Users\25671\source\repos\Sixth\Sixth\Program.cs:line 12
程序执行结束
差不多了,就到重载了
重载
定义
重载,简单来讲,是指我们可以定义一些名称相同的方法,通过定义不同的输入参数来区分这些方法,然后再调用时,我们可以根据参数类型不同去选择我们所需要的
这样说有点过于广泛,在C#里,重载主要就分俩类,分别为方法(函数)重载和运算符重载:
方法(函数)重载
顾名思义,就是搞多个在同一类中相同名称相同但参数类型不同的方法,举个例子,就是俩个长的像的双胞胎,但一个只会打代码,一个只会画画一样
其实就这点,我就直接讲C#里比较重要的一部分了
运算符重载
这里算是C#算是重要的一部分,我这里就简单先用微软文档说明下(原链接:运算符重载 - 定义一元运算符、算术运算符、相等运算符和比较运算符。 - C# reference | Microsoft Learn):
用户定义的类型可以重载预定义的 C# 运算符。 也就是说,当操作数之一或者两个操作数都属于该类型时,该类型可以提供操作的自定义实现。 “可重载运算符”部分显示哪些 C# 运算符可以重载。
使用
operator关键字声明运算符。 运算符声明必须满足以下规则:
它包括一个
public修饰符。一元运算符有一个输入参数。 二进制运算符有两个输入参数。 在每种情况下,至少有一个参数必须具有类型
T或T?,其中T是包含运算符声明的类型。它包括
static修饰符,复合赋值运算符除外,例如+=。增量(
++)和递减(--)运算符可以作为静态方法或实例方法实现。 实例方法运算符是 C# 14 中引入的新功能。
总之,根据微软文档的意思,他就是便于C#混合部分/全部参数的,而且他是靠operator 关键字实现的
示例
方法重载、函数重载在其他语言里也经常见到,我就只演示运算符重载了(这里只演示一种):
namespace Sixth
{
internal class Program
{
static void Main(string[] args)
{
//重载
//创建Area类的两个对象,并初始化宽度和高度
Area a1 = new Area { width = 10, height = 10 };
Area a2 = new Area { width = 20, height = 20 };
//把a1和a2相加,实际上是调用了operator +方法
Area a3 = a1 + a2;
//输出面积
Console.WriteLine("A1的面积是:" + a1.getArea());
Console.WriteLine("A2的面积是:" + a2.getArea());
Console.WriteLine("A3的面积是:" + a3.getArea());
}
class Area()
{
public int width;
public int height;
//计算面积的方法
public int getArea()
{
return width * height;
}
//重载加号
public static Area operator +(Area t1, Area t2)
{
Area t = new Area();
t.width = t1.width + t2.width;
t.height = t1.height + t2.height;
return t;
}
}
}
}这里是结果:
A1的面积是:100
A2的面积是:400
A3的面积是:900
以防有人会很懵,这里还是简单说明下,A1的长和宽都是10,A2长和宽都是20,而A3的长宽是A1和A2的和,所以长宽是30,由此可得上述结果
类型拓展
定义
简单来讲,就是拓展一些无法修改源代码的类型,就这样
由于这样讲内容可能过少,我可以引用下微软的文档(原链接:扩展成员 - C# | Microsoft Learn):
使用扩展成员可以“添加”方法到现有类型,而无需创建新的派生类型、重新编译或其他修改原始类型
示例
由于入门后用到的情况不多,我这里就只简单演示下它怎么用:
namespace Sixth
{
internal class Program
{
static void Main(string[] args)
{
//类型拓展
float number = 123.456f;
int intValue = number.ToInt();
//输出
System.Console.WriteLine(number);
System.Console.WriteLine(intValue);
}
}
//类型拓展类
public static class FloatExtensions
{
public static int ToInt(this float value)
{
return (int)value;
}
}
}这里是结果:
123.456
123
这样,可算是完事了,正式入门完成