前言

可算是入门结束了,真是累的要命啊,终于要开始写游戏了(当然,这是入门之后的事了)

总之如你们所见,就这么点内容,不过还是挺基础、重要的,毕竟还算是基本功

以上的内容虽然看着多,但实际上如果学过其他相关的语言(如java,py),你可以简单省略异常处理和重载这部分,看完换言之也就差不多完事了

反射

定义

本质上就是获取代码信息的:主要就看三种:一种是类型信息,一种是变量,一种是函数

虽然你们能知道有很多,比如程序集,但目前没必要

不过微软文档倒是回答挺多的(原链接:.NET 中的反射 | Microsoft Learn

命名空间中的System.Reflection类以及System.Type可用于获取有关加载的程序集及其中定义的类型的信息,例如类、接口和值类型(即结构和枚举)。 还可以使用反射在运行时创建类型实例,以及调用和访问它们。

程序集 包含模块、模块包含类型和类型包含成员。 反射提供封装程序集、模块和类型的对象。 可以使用反射动态创建类型的实例、将类型绑定到现有对象或从现有对象获取类型。 然后,可以调用该类型的方法或访问其字段和属性。

用法

实际上这玩意还有很多类型,不过一般来说没人会深入写这种,主要是没太大必要,至少入门是这样的,因此我就简单过一遍,虽然微软官方有很多标准用法,以下便是其用法(原链接:.NET 中的反射 | Microsoft Learn):

  1. 用于 Assembly 定义和加载程序集、加载程序集清单中列出的模块,并从此程序集中找到类型并创建它的实例。

  2. 使用 Module 可发现包含模块的程序集以及模块中的类等信息。 还可以获取模块上定义的所有全局方法或其他特定非全局方法。

  3. ConstructorInfo 发现构造函数的相关信息,例如名称、参数、访问修饰符(如 publicprivate)以及实现详细信息(如 abstractvirtual)等。 使用 GetConstructorsGetConstructorType 方法来调用特定构造函数。

  4. 用于 MethodInfo 发现方法的名称、返回类型、参数、访问修饰符和实现详细信息(如 abstractvirtual)等信息。 使用GetMethods中的GetMethodType方法调用特定方法。

  5. 使用 FieldInfo 以发现字段的名称、访问修饰符和static等实现详细信息,以及获取或设置字段值。

  6. 使用 EventInfo 来发现事件的名称、事件处理程序的数据类型、自定义属性、声明类型和反射的类型等信息,以及添加或删除事件处理程序。

  7. 用于 PropertyInfo 发现诸如名称、数据类型、声明类型、反射类型以及属性的只读或可写状态等信息,以及获取或设置属性值。

  8. 用于 ParameterInfo 发现参数的名称、数据类型、参数是输入参数还是输出参数以及参数在方法签名中的位置等信息。

  9. 当你在 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(捕获异常)里,可分为三种,分别为:

  1. try-catch:尝试执行-捕获异常

  1. try-finally:无论是否异常,finally强制执行

  2. 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修饰符。

  • 一元运算符有一个输入参数。 二进制运算符有两个输入参数。 在每种情况下,至少有一个参数必须具有类型TT?,其中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

这样,可算是完事了,正式入门完成

这个是屑