50、C 泛型与事件编程全解析

C# 泛型与事件编程全解析

1. 泛型基础与 T 的含义

在编程中,若为 int 类型编写 LogInt 函数,即便先将 GameObject 转换为 int ,该函数也无法正常工作,毕竟僵尸可不是简单的数字。这表明不同类型不应随意混合使用。若没有泛型,对于每种能转换为字符串并在控制台面板记录的类型,都需编写不同的函数,这既浪费时间又不切实际。而泛型 <T> 的出现解决了这一问题。

我们使用标识符 T 表示函数中预期在其他地方复用的类型,这只是一种约定,并非严格规则。我们也可以打破这个约定,用其他类型标识符编写相同的函数,但这并不合适。 T 作为一种约定,自首次实现以来一直沿用,就像 for 循环通常写成 for (int i; i < 10; i++) 一样, int i 看起来很合适,推测 i 可能来自 integer index 。遵循 T 这个约定能让其他程序员更易理解代码意图。

在实际应用中,当多个类相互继承行为时,调用某个函数时,可能希望某个类的反应与其他类不同。例如,用木桩刺穿吸血鬼心脏和僵尸心脏的效果截然不同,调用相同的 OnStakedThroughHeart() 函数应执行不同的代码。通常会让僵尸和吸血鬼使用包含 OnStakedThroughHeart 事件的接口,但玩家角色可能并不关心木桩刺入的是哪种怪物,这就带来了问题,运行玩家的代码可能不想检查每种类型并根据遇到的不同类型决定操作。

2. 泛型函数的使用

泛型类型的一个好处是可以自定义数据类型,如吸血鬼或僵尸,将其用作泛型类型。下面通过一个简单的例子来展示泛型函数的使用,首先创建一个简单的僵尸类,然后使用泛型函数交换两个变量的数据。

2.1 交换函数示例

创建一个简单的泛型函数 Swap<T> ,代码如下:

public static void Swap<T>(ref T first, ref T second)
{
    T temp = second;
    second = first;
    first = temp;
}

使用示例:

int first = 7;
int second = 13;
Console.WriteLine($"{first} and {second}");
Swap(ref first, ref second);
Console.WriteLine($"{first} and {second}");

string[] array = { "First", "Second" };
Console.WriteLine($"{array[0]} and {array[1]}");
Swap(ref array[0], ref array[1]);
Console.WriteLine($"{array[0]} and {array[1]}");

上述代码先打印出交换前的数值,然后调用 Swap 函数进行交换,再打印交换后的数值。对于字符串数组同样适用,无需对 Swap 泛型函数做额外修改。

2.2 自定义数据类型

创建一个 GenericHumanoid 类,存储 Name 变量,重写 ToString() 函数,代码如下:

public class GenericHumanoid
{
    public string Name;

    public GenericHumanoid(string name)
    {
        Name = name;
    }

    public override string ToString()
    {
        return $"A Zombie or Vampire named {Name}";
    }
}

使用 UseSwapGenerics() 函数:

public static void UseSwapGenerics()
{
    GenericHumanoid vampire = new GenericHumanoid("D");
    GenericHumanoid zombie = new GenericHumanoid("Stubbs");
    Console.WriteLine($"{vampire} and {zombie}");
    Swap(ref vampire, ref zombie);
    Console.WriteLine($"{vampire} and {zombie}");
}

当尝试混合不同数据类型时,如使用字符串和 GenericZombie 调用 Swap 函数,会出现错误:“The type arguments for method ‘Generics.Swap (ref T, ref T)’ cannot be inferred from the usage. Try specifying the type arguments explicitly.” 这表明不同类型不能随意混合,C# 需要明确的类型信息才能解析代码,因此要确保类型匹配。

3. 泛型类型的创建与使用

泛型类型可以创建没有特定初始类型的数据类型,在以特定方式组织数据时非常有用。例如,若要创建一个包含三个僵尸的短列表,没必要将数据类型局限于僵尸。

3.1 创建泛型类

创建一个新类 ThreeThings<T> ,其构造函数用于保存三个相同类型的对象,代码如下:

public class ThreeThings<T>
{
    public T First;
    public T Second;
    public T Third;

    public ThreeThings(T first, T second, T third)
    {
        First = first;
        Second = second;
        Third = third;
    }
}

3.2 使用泛型类

Start() 函数中创建 ThreeThings 对象并赋值:

void Start()
{
    // 错误示例
    // ThreeThings things = new ThreeThings(new GenericZombie("Z1"), new GenericZombie("Z2"), new GenericZombie("Z3"));

    // 正确示例
    ThreeThings<GenericZombie> things = new ThreeThings<GenericZombie>(new GenericZombie("Z1"), new GenericZombie("Z2"), new GenericZombie("Z3"));
}

使用泛型类的构造函数时需要指定类型参数,否则会出错。例如,要告诉 ThreeThings 它将处理的是 <GenericZombie> 类型。

4. var 关键字的使用

通常,我们会显式地创建类型,如 zombie firstZombie = new zombie("stubbs") int a = 10; 。但当类型不确定且事先不知道会是什么类型时, var 关键字就派上用场了。

4.1 var 的基本使用

使用 var 关键字可以让计算机自动推断变量的类型,示例如下:

var whatAmI = 1;
Console.WriteLine(whatAmI.GetType());

运行上述代码,控制台会输出 System.Int32 ,表明 whatAmI 被推断为整数类型。

4.2 var 的局限性

虽然 var 可以处理任何数据类型,但也有局限性。过度使用 var 会影响代码的清晰度,例如:

var imAFloat = 11.8f;
Console.WriteLine((int)imAFloat);

这里使用 var 声明 imAFloat ,但从命名看容易产生误解。如果一开始就使用 int 声明,就能立刻发现命名与实际类型不符的问题。

对于泛型类,使用 var 时编译器会进行类型检查。例如,创建一个泛型类 Stuff<T>

public class Stuff<T>
{
    public T thing;

    public void AssignThing(T newThing)
    {
        thing = newThing;
    }
}

尝试将 float 1.0f 赋给 Stuff<int> 会报错:

Stuff<int> stuff = new Stuff<int>();
// 错误示例
// stuff.AssignThing(1.0f);

这说明泛型类型在赋值后会变成指定的类型,必须按此类型使用。泛型类创建时是开放类型,赋值后成为封闭类型,且赋值后不能重新赋值。

5. 多个泛型值的使用

当可能需要分配多种类型时,作为优秀的程序员,应考虑在泛型类中使用多种类型。可以使用 <T, U> 来分配两种类型。

5.1 多泛型类示例

public class TwoThings<T, U>
{
    public T FirstThing;
    public U SecondThing;

    public TwoThings(T first, U second)
    {
        FirstThing = first;
        SecondThing = second;
    }
}

5.2 多泛型函数示例

public static void LogTwoThings<T, U>(T first, U second)
{
    Console.WriteLine($"First: {first}, Second: {second}");
}

使用示例:

TwoThings<Zombie, int> zombieWithRank = new TwoThings<Zombie, int>(new Zombie(), 2);
LogTwoThings(zombieWithRank.FirstThing, zombieWithRank.SecondThing);

多个泛型值的使用能完成更多复杂任务,但每个实例分配类型后,它们不一定能按预期方式相互交互。同时,虽然使用 var 作为返回类型看似方便,但 C# 有其他机制处理, var 并不适用于此。

6. 匿名对象

在实际编程中,应清楚所处理的数据类型。但在打包数据在对象间传递时,有时跳过编写结构体或类,使用更方便的方式会更好,匿名对象就满足了这一需求。

6.1 匿名对象的创建

使用 new {} 语法将匿名对象赋值给 var ,示例如下:

var anonymousObj = new { someInt = 3, someString = "Hello" };

6.2 匿名对象的传递与使用

匿名对象可以通过 System.Object 类型传递给函数,但由于不知道每个类型,解包数据时可能会出错。虽然可以使用对象作为返回类型,但在实践中应避免,因为强类型是 C# 的优势,滥用对象类型会破坏这一优势。

匿名对象的属性通常是只读的,一旦对象创建就不能更改,但并非所有属性都是只读的,普通旧数据(POD)类型会变成只读,引用类型对象则可正常使用。例如:

var queueObj = new { queue = new Queue<int>() };
queueObj.queue.Enqueue(1); // 可以正常使用

匿名对象适用于声明类型繁琐的有限场景,当需要在两个相邻函数间传递大量数据时,它可能更方便,但使用时要留下清晰的注释,以便他人理解使用原因。

下面通过一个表格总结泛型和匿名对象的相关特性:
| 特性 | 描述 |
| ---- | ---- |
| 泛型 <T> | 解决类型复用问题,避免为每种类型编写不同函数,提高代码复用性 |
| T 的约定 | 作为一种约定,方便程序员理解代码意图 |
| 泛型函数 | 可处理多种数据类型,如 Swap<T> 函数 |
| 泛型类型 | 可创建无特定初始类型的数据类型,使用时需指定类型参数 |
| var 关键字 | 自动推断变量类型,提高代码灵活性,但可能影响清晰度 |
| 多个泛型值 | 使用 <T, U> 处理多种类型,完成更复杂任务 |
| 匿名对象 | 方便打包数据传递,属性通常只读,适用于特定场景 |

7. 总结

泛型是一种强大的编程工具,能显著提高代码的复用性和灵活性。但在使用时也需谨慎,避免过度使用导致代码难以调试和维护。在实际编程中,应根据具体需求合理选择是否使用泛型、 var 关键字和匿名对象。

8. 事件处理

对于许多简单任务,Unity 3D 提供了一些有用的函数,用于更新对象并在对象首次创建时执行。同时,UI 中也有鼠标点击的事件处理程序。

当对象接收到执行条件时,操作通常局限于该对象。虽然利用已掌握的工具可以传播消息,但过程可能很繁琐,且会使用各种静态函数,导致代码像“意大利面条”一样,函数调用相互依赖,难以调试。

下面是一个简单的 mermaid 流程图,展示对象接收条件和执行操作的流程:

graph TD;
    A[对象接收条件] --> B{是否满足条件};
    B -- 是 --> C[执行操作];
    B -- 否 --> D[等待条件];
    C --> E[操作结束];

总之,在编程中要合理运用泛型和事件处理机制,以提高代码的质量和可维护性。

9. 泛型的使用建议与注意事项

9.1 泛型使用的时机

泛型并非在所有情况下都适用,只有在没有其他替代方案时才应考虑使用。例如,当需要为不同类型编写类似功能的函数时,泛型可以避免代码的重复编写。但如果只是处理单一类型的数据,使用泛型反而会增加代码的复杂度。

9.2 避免过度使用泛型

过度使用泛型会导致代码中充斥着 var <T> ,使得代码的类型冲突难以追踪,调试变得困难,浪费大量时间。因此,在使用泛型时要适度,确保代码的可读性和可维护性。

9.3 遵循约定

在使用泛型时,应遵循 T 作为泛型类型标识符的约定,这样可以让其他程序员更容易理解代码的意图。虽然可以使用其他标识符,但会增加代码的理解成本。

9.4 明确类型

在使用泛型类和函数时,要明确指定类型参数,避免类型推断错误。例如,在使用 ThreeThings<T> 类时,要明确告诉它处理的是哪种类型。

下面通过一个列表总结泛型使用的注意事项:
1. 只有在必要时使用泛型。
2. 避免过度使用泛型,以免增加调试难度。
3. 遵循 T 作为泛型类型标识符的约定。
4. 明确指定泛型类型参数,避免类型推断错误。

10. 泛型在 Unity 中的应用

在 Unity 中,泛型也有广泛的应用。例如, GameObject 类的 GetComponent<T>() 函数就是一个泛型函数,用于获取游戏对象上的组件。

10.1 GetComponent<T>() 函数的使用

using UnityEngine;

public class ComponentExample : MonoBehaviour
{
    void Start()
    {
        // 获取 Transform 组件
        Transform transformComponent = GetComponent<Transform>();
        if (transformComponent != null)
        {
            // 设置 Transform 组件的位置
            transformComponent.position = new Vector3(1, 1, 1);
        }

        // 另一种使用方式
        Transform anotherTransform = (Transform)GetComponent(typeof(Transform));
        if (anotherTransform != null)
        {
            anotherTransform.rotation = Quaternion.Euler(0, 90, 0);
        }
    }
}

上述代码展示了 GetComponent<T>() 函数的两种使用方式,一种是使用泛型语法,另一种是使用 typeof 关键字。泛型语法更加简洁,推荐使用。

10.2 泛型在 Unity 中的优势

使用泛型可以提高代码的可读性和可维护性,避免了类型转换的麻烦。例如,在获取组件时,使用泛型可以直接得到所需类型的组件,而不需要进行强制类型转换。

下面是一个 mermaid 流程图,展示 GetComponent<T>() 函数的使用流程:

graph TD;
    A[获取 GameObject] --> B[调用 GetComponent<T>()];
    B --> C{是否获取到组件};
    C -- 是 --> D[使用组件];
    C -- 否 --> E[处理未获取到组件的情况];

11. 事件处理的优化

11.1 避免使用静态函数传播消息

在 Unity 中,使用静态函数传播消息会导致代码的耦合度增加,难以调试和维护。可以使用事件系统来替代静态函数,实现消息的传播。

11.2 使用事件系统

事件系统可以让对象之间的通信更加灵活和松耦合。例如,创建一个事件类,用于处理对象之间的消息传递。

using UnityEngine;
using System;

// 定义事件类
public class EventManager : MonoBehaviour
{
    public static event Action OnSomethingHappened;

    public static void TriggerEvent()
    {
        if (OnSomethingHappened != null)
        {
            OnSomethingHappened();
        }
    }
}

// 订阅事件的类
public class EventSubscriber : MonoBehaviour
{
    void Start()
    {
        EventManager.OnSomethingHappened += HandleEvent;
    }

    void OnDestroy()
    {
        EventManager.OnSomethingHappened -= HandleEvent;
    }

    void HandleEvent()
    {
        Debug.Log("Event handled!");
    }
}

在上述代码中, EventManager 类定义了一个静态事件 OnSomethingHappened ,并提供了一个触发事件的方法 TriggerEvent() EventSubscriber 类订阅了该事件,并在事件触发时执行相应的操作。

11.3 事件处理的优势

使用事件系统可以避免代码的“意大利面条”现象,提高代码的可维护性和可扩展性。同时,事件系统可以让对象之间的通信更加清晰和明确。

下面通过一个表格总结事件处理的优化方法:
| 优化方法 | 描述 |
| ---- | ---- |
| 避免使用静态函数 | 减少代码耦合度,提高可维护性 |
| 使用事件系统 | 实现对象间的灵活通信,降低耦合度 |
| 订阅和取消订阅事件 | 确保事件处理的正确性和资源的释放 |

12. 总结与展望

12.1 总结

泛型和事件处理是编程中非常重要的概念。泛型可以提高代码的复用性和灵活性,避免代码的重复编写;事件处理可以实现对象之间的通信,提高代码的可维护性和可扩展性。但在使用泛型和事件处理时,要注意避免过度使用,确保代码的清晰度和可维护性。

12.2 展望

随着编程技术的不断发展,泛型和事件处理的应用场景会越来越广泛。未来,可能会出现更加高效和灵活的泛型和事件处理机制,进一步提高代码的质量和开发效率。

在实际编程中,要不断学习和掌握泛型和事件处理的知识,根据具体需求合理运用这些技术,以提高自己的编程水平。

总之,合理运用泛型和事件处理机制,可以让代码更加简洁、高效和易于维护,为开发高质量的软件打下坚实的基础。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值