C#接口与抽象类
Dependency Management in C# ClassesC#类中的依赖管理#
Projects where classes create everything they need by themselves often have common architectural problems, because they handle their own dependencies directly. For example, a class might internally create services like a custom player HUD or input settings. This causes several issues:在类自行创建所需所有对象的项目中,通常会存在一些常见的架构问题,因为它们直接处理自身的依赖关系。例如,一个类可能会在内部创建诸如自定义玩家 HUD 或输入设置等服务。这会引发以下几个问题:
- Tight Coupling: Using specific implementations directly within a class creates tight connections between the class and those implementations. Modifying or replacing these dependencies, such as changing an input handler from keyboard to gamepad, inevitably requires changing the dependent class itself, violating the open-closed principle.紧耦合:在类中直接使用特定实现会导致类与这些实现之间形成紧密连接。修改或替换这些依赖项(例如将输入处理器从键盘更改为手柄)不可避免地需要修改依赖类本身,这违反了开闭原则。
- Poor Maintainability: Dependency setup logic often becomes duplicated across the application. If multiple components individually configure the same dependency, it becomes difficult to manage and understand the configurations, and thus harder to maintain.可维护性差:依赖项的设置逻辑通常会在应用程序中重复出现。如果多个组件分别配置相同的依赖项,管理和理解这些配置会变得困难,从而难以维护。
- Difficulty in Unit Testing: Hard-coded dependencies severely limit testability. For example, when a class internally creates a complex dependency like an audio manager, it becomes difficult to replace it with a mock during testing, which makes unit tests less reliable.单元测试困难:硬编码的依赖项会严重限制可测试性。例如,当一个类在内部创建了一个复杂的依赖项(如音频管理器)时,很难在测试时用模拟对象替换它,这会降低单元测试的可靠性。
- Reduced Reusability and Modularity: Components tied to certain dependencies become less flexible. This makes it harder to reuse them in different situations. If a component is directly linked to a specific manager or subsystem, you can't easily move it or use it somewhere else.可重用性和模块化降低:绑定到某些依赖项的组件会变得不够灵活。这使得在不同情境下重用它们变得更加困难。如果一个组件直接关联到特定的管理器或子系统,就无法轻松地移动或在其他地方使用它。
Dependency Injection依赖注入#
Dependency Injection (DI) is a way to write code that's flexible and easy to manage. Rather than creating or directly referencing its dependencies, a class receives them from outside.依赖注入(DI)是一种编写灵活且易于管理代码的方式。类不直接创建或引用其依赖项,而是从外部接收它们。
Classes depend on an abstraction (an interface or abstract class) rather than a concrete implementation. This approach adheres to the Dependency Inversion Principle (DIP), which states that high-level modules should not depend on low-level modules; both should depend on abstractions. Moreover, abstractions themselves should not depend on details – the details should depend on the abstraction.类依赖于抽象(接口或抽象类)而不是具体的实现。这种方法遵循依赖倒置原则(DIP),即高层模块不应依赖于低层模块,两者都应依赖于抽象。此外,抽象本身不应依赖于细节,而细节应依赖于抽象。
By following DIP, you reduce direct coupling and make it easier to replace components without affecting higher-level logic.通过遵循DIP,可以减少直接耦合,并更容易在不影响高层逻辑的情况下替换组件。
To address issues above and build more flexible, testable, and maintainable systems in UNIGINE, you can use components that implement interfaces or inherit from abstract classes. C# Component System supports this approach and makes it easier to program to abstractions rather than concrete implementations and decouple components from specific dependencies.为了解决上述问题并在UNIGINE中构建更灵活,可测试和可维护的系统,可以使用实现接口或继承抽象类的组件。C#组件系统支持这种方法,并使得基于抽象而非具体实现进行编程变得更加容易,同时解耦组件与特定依赖项的关系。
Interfaces接口#
Interfaces are a mechanism for defining contracts that classes can implement. Any class that implements an interface agrees to provide that functionality.接口是一种定义契约的机制,类可以通过实现接口来遵守这些契约。任何实现接口的类都承诺提供相应的功能。
Using interfaces for DI is a common pattern to achieve maximum decoupling. The approach to working with interfaces in UNIGINE can be summarized as follows:使用接口进行 DI 是实现最大解耦的常见模式。在 UNIGINE 中使用接口的方法可以总结如下:
-
Define an Interface.定义接口
Identify the behavior or functionality that needs to be provided, and define an interface declaring the required methods. This interface serves as a contract that any specific component must fulfill.确定需要提供的功能或行为,并定义一个声明所需方法的接口。该接口作为任何具体组件必须遵守的契约。
源代码 (C#)public interface IShootable { public void Shoot(); }
注意You can create an empty *.cs file right in the Asset Browser window.您可以直接在 Asset Browser 窗口中创建一个空的*.cs文件。 -
Implement the Interface in a Component.在组件中实现接口
Create one or more components that implement this interface. They will contain the actual code to perform the work, but from the outside they will be accessed via the interface. You can have multiple implementations of the same interface coexisting, which is a powerful way to swap behaviors. The component that implements the interface is called a service.创建一个或多个实现该接口的组件。这些组件将包含实际执行工作的代码,但从外部访问时将通过接口进行。您可以同时拥有同一接口的多个实现,这是一种强大的行为交换方式。实现接口的组件称为服务。
源代码 (C#)public class WizardStaff : Component, IShootable { public void Shoot() { Log.MessageLine("The wizard's staff shot a fireball."); } } public class MagicWand : Component, IShootable { public void Shoot() { Log.MessageLine("The magic wand shot an electric zap."); } } public class Bow : Component, IShootable { public void Shoot() { Log.MessageLine("The bow shot an arrow."); } }
-
Depend on the Interface in the Client.依赖接口的客户端组件
Any component that needs to use the special behavior should not directly instantiate concrete service class. Instead, it should rely on a reference to an interface type. The component that depends on the service is called a client.任何需要使用特定行为的组件都不应直接实例化具体的服务类,而应依赖于接口类型的引用。依赖服务的组件称为客户端。
源代码 (C#)public class Player : Component { [ShowInEditor] IShootable mainPlayerWeapon = null; [ShowInEditor] IShootable sparePlayerWeapon = null; void Update() { if (Input.IsMouseButtonDown(Input.MOUSE_BUTTON.LEFT)) { mainPlayerWeapon?.Shoot(); } if (Input.IsMouseButtonDown(Input.MOUSE_BUTTON.RIGHT)) { sparePlayerWeapon?.Shoot(); } } }
-
Inject the Concrete Implementation.注入具体实现
This step is where the wiring happens. The interface reference in the client must be assigned a concrete object instance that implements the interface. There are a couple of ways to do this using UNIGINE features:这一步是实际“连接”依赖的过程。客户端中的接口引用必须被赋值为实现了该接口的具体对象实例。在 UNIGINE 中,可以通过以下两种方式实现:
-
Via the Editor. In UNIGINE you can expose the interface field in the component parameters. By default, parameters are displayed or hidden in the UI in accordance with access modifiers: public - displayed, otherwise - hidden. But you can show a private or protected one by specifying the corresponding visibility attribute ShowInEditor as shown above in the code sample.通过编辑器:在UNIGINE中,您可以在组件参数中公开接口字段。默认情况下,参数根据访问修饰符在UI中显示或隐藏:public显示,其他隐藏。但可以通过添加ShowInEditor属性强制显示私有或受保护字段。
Then, you simply assign a node with an assigned component that implements your specific interface.然后只需附加一个挂载了实现该接口的组件的节点。
-
Via the API. Or you can locate the interface using the engine API.通过API:也可以通过引擎API查找接口实现。
源代码 (C#)// Getting IShootable var newWeapon = obj.GetComponent<IShootable>(); // If newWeapon is not null, it will be new main player weapon mainPlayerWeapon = newWeapon ?? mainPlayerWeapon;
A common pattern is calling GetComponent<>() method in the client's code. The engine will search for a component that implements the IShootable interface, as in our example, and return a reference to it.常见模式是在客户端代码中调用GetComponent<>()方法。引擎会搜索实现目标接口(如IShootable)的组件并返回其引用。
The key point is that the client code does not instantiate the service directly - instead, it receives a reference or obtains it from the environment.关键点:客户端代码不直接实例化服务,而是从环境中获取引用。
-
Abstract Classes抽象类#
Interfaces aren't the only way to invert dependencies – abstract classes can also serve as the abstraction layer between a client and the concrete implementation. The workflow for using abstract classes is very similar to the interface approach, with a few differences:接口不是依赖倒置的唯一方式,抽象类也可作为客户端和具体实现间的抽象层。使用抽象类的工作流程与接口类似,但有区别:
-
Define an Abstract Base Class.定义抽象基类
Create an abstract class that declares the necessary methods and optionally provides common functionality or default behavior shared across implementations. Since it's abstract, it can't be instantiated directly - instead, it serves as a template that concrete subclass components must follow.创建抽象类声明必要方法,可选择提供共享功能或默认行为。由于是抽象的,不能直接实例化,而是作为具体子类组件的模板。
源代码 (C#)using System.Collections; using System.Collections.Generic; using Unigine; [Component(PropertyGuid = "AUTOGENERATED_GUID")] public abstract class Toggleable : Component { [ShowInEditor] private bool isToggled = false; public bool Toggled { get => isToggled; set { if (value != isToggled) { bool ok = value ? On() : Off(); isToggled = isToggled ^ ok; } } } public bool Toggle() => isToggled = isToggled ^ (isToggled ? Off() : On()); protected abstract bool On(); protected abstract bool Off(); }
-
Create Subclasses that Inherit from the Abstract Class.创建继承自抽象类的子类
Implement one or more classes that extend the abstract base class. Each subclass must implement the abstract methods, providing its own behavior.实现一个或多个扩展抽象基类的子类。每个子类必须实现抽象方法并提供自己的行为逻辑。
源代码 (C#)using System.Collections; using System.Collections.Generic; using Unigine; [Component(PropertyGuid = "AUTOGENERATED_GUID")] public class Lamp : Toggleable { [ParameterColor] public vec4 emission_color = vec4.WHITE; protected override bool On() { Log.MessageLine("Lamp::On()"); return SetEmissionColor(emission_color); } protected override bool Off() { Log.MessageLine("Lamp::Off()"); return SetEmissionColor(vec4.ZERO); } private bool SetEmissionColor(vec4 emission_color) { Object obj = (Object)node; if (obj == null) return false; for (var surface = 0; surface < obj.NumSurfaces; surface += 1) obj.SetMaterialParameterFloat4("emission_color", emission_color, surface); return true; } private void Init() { SetEmissionColor(Toggled ? emission_color : vec4.ZERO); } }
源代码 (C#)using System.Collections; using System.Collections.Generic; using Unigine; [Component(PropertyGuid = "AUTOGENERATED_GUID")] public class Fan : Toggleable { public float rotation_speed = 120; private float target_speed = 0; private float actual_speed = 0; protected override bool On() { Log.MessageLine("Fan::On()"); target_speed = rotation_speed; return true; } protected override bool Off() { Log.MessageLine("Fan::Off()"); target_speed = 0; return true; } private void Init() { target_speed = Toggled ? rotation_speed : 0; } private void Update() { actual_speed = MathLib.Lerp(actual_speed, target_speed, Game.IFps); node.Rotate(0, 0, actual_speed * Game.IFps); } }
-
Inject a Concrete Subclass Instance into the Client Class.将具体子类实例注入客户端
Similar to the interface case, the client that needs the functionality holds a reference of the abstract base class type. Then, because of polymorphism, you can pass specific implementations of the abstract class to the client class in two following ways:与接口的情况类似,需要该功能的客户端持有一个抽象基类类型的引用。然后,由于多态性,您可以通过以下两种方式将抽象类的具体实现传递给客户端类:
-
Via the Editor. Simply assign a node with the assigned component, that inherits your abstract base class in a node Parameters window:通过编辑器:只需附加一个带有附加组件的节点,该组件在节点参数窗口中继承您的抽象基类。
-
Via the API. Retrieving abstract base classes via the API works in a similar way as interfaces.通过API:检索抽象基类的方式与接口类似。
源代码 (C#)public class Toggler : Component { private void Update() { // Some logic for intersection with togglable object... if (obj) { var toggleable = obj.GetComponent<Toggleable>(); if (toggleable) { toggleable.Toggle(); } } } }
In this case, the client code requests a component that derives from an abstract class. The engine traverses the available components and returns one that matches the expected base type, such as Toggleable.在这种情况下,客户端代码请求派生自抽象类的组件,引擎遍历可用组件并返回匹配的基类类型(如Toggleable)。
-
Choosing the Right Abstraction Mechanism选择合适的抽象机制#
Interfaces and abstract classes both define behaviour without binding to concrete implementations, but they differ in intent and capabilities:接口和抽象类都定义行为而不绑定具体实现,但用途不同:
- Interfaces are ideal for defining behaviour that can be implemented by any class, regardless of its place in the hierarchy. They also support multiple inheritance by allowing a class to implement multiple interfaces.接口是定义可以由任何类实现的行为的理想选择,无论其在层次结构中的位置如何。它们还通过允许一个类实现多个接口来支持多重继承。
- Abstract classes are more appropriate when there is a need to share common implementation details, such as fields or methods across a group of related types.当需要跨一组相关类型共享公共实现细节(例如字段或方法)时,抽象类更合适。
It's important to use abstractions when they bring clear benefits - such as easier testing, multiple implementations, or shared logic. But if there's only one concrete use case and no need for reuse or substitution, keeping things simple is often the better choice.当抽象带来明显的好处时,使用抽象非常重要-例如更容易测试,多个实现或共享逻辑。但是,如果只有一个具体的用例,并且不需要重用或替换,那么保持简单通常是更好的选择。
Using External Dependency Injection Frameworks使用外部依赖注入框架#
You can configure dependencies manually within the engine - using the editor and components to wire things together. This method gives full control over how and when dependencies are resolved. Alternatively, external Dependency Injection frameworks can be used. They allow you to:您可以在引擎中手动配置依赖关系:使用编辑器和组件将事物连接在一起。此方法可以完全控制如何以及何时解决依赖关系。或者,可以使用外部依赖注入框架。它们能:
- Use familiar tools from the .NET ecosystem.利用.NET生态系统中熟悉的工具
- Simplify dependency management in larger projects.简化大型项目中的依赖管理
- Access advanced DI features like scopes, signals, or event-based injection.访问高级依赖注入功能,如范围、信号或基于事件的注入
Below are examples of how to set up some popular external DI frameworks.下面是如何设置一些流行的外部依赖注入框架的例子。
Setting up Zenject in a UNIGINE Project在UNIGINE项目中设置Zenject#
- Download the non-Unity build from the official Zenject GitHub repository从官方的Zenject GitHub库下载非Unity版本
- Extract the downloaded archived *.dll files into your project's bin folder的*.dll文件,解压到项目的bin文件夹
-
Add references to the *.dll files in your project:向您的项目添加对*.dll文件的引用:
-
Option 1: Using Your IDE选项1:使用您的IDE
- Open your project in your preferred IDE.在您首选的IDE中打开项目。
- Open the Reference Manager or the equivalent interface for managing project references.打开引用管理器或等效的项目引用管理界面。
- Use the Browse option to locate the required DLLs in the project's bin folder.使用浏览选项定位到项目bin文件夹中所需的DLL文件。
- Select previously added files.选择先前添加的文件。
- Confirm the selection to add them as references in your project.确认选择以将它们作为引用添加到您的项目中。
-
Option 2: Manually edit the *.csproj file选项2:手动编辑*.csproj文件
Open your project *.csproj file and add:打开您的项目*.csproj文件并添加:
源代码 (XML)<ItemGroup> <Reference Include="Zenject"> <HintPath>bin\Zenject.dll</HintPath> </Reference> <Reference Include="Zenject-Signals"> <HintPath>bin\Zenject-Signals.dll</HintPath> </Reference> <Reference Include="Zenject-usage"> <HintPath>bin\Zenject-usage.dll</HintPath> </Reference> </ItemGroup>
-
- Restart your IDE (and Editor, if needed) to ensure the changes are recognized.重新您的IDE(如果需要,也重启编辑器),以确保更改被识别。
-
You're now ready to use the Zenject in your project. For usage details, refer to the Zenject documentation.您现在可以在项目中使用Zenject了。有关使用详情,请参阅Zenject文档。
源代码 (C#)public interface ISomeInterface { void Send(string message); } public class SomeImplementation : Component, ISomeInterface { public void Send (string message) { Log.MessageLine(message); } }
源代码 (C#)using System.Collections; using System.Collections.Generic; using Unigine; using Zenject; [Component(PropertyGuid = "AUTOGENERATED_GUID")] public class ClientCode : Component { void Init() { var container = new DiContainer(); container.Bind<ISomeInterface>().To<SomeImplementation>().AsSingle(); var foo = container.Resolve<ISomeInterface>(); foo.Send("Hello, Zenject!"); } }
Setting up Microsoft DI in a UNIGINE Project在UNIGINE项目中设置Microsoft依赖注入#
- Open your project in your preferred IDE.在您首选的IDE中打开项目。
-
Install the NuGet package for Microsoft.Extensions.DependencyInjection:安装Microsoft.Extensions.DependencyInjection的NuGet包:
- Via the built-in IDE NuGet Package Manager, or通过IDE内置的NuGet Package Manager安装,或
-
Using the .NET CLI command:使用.NET CLI命令安装:
源代码dotnet add package Microsoft.Extensions.DependencyInjection
-
That's it - Microsoft's DI framework is now available in your project. You can build more complex setups using scopes, lifetimes, and modules - see the official docs for details.这样就完成了:Microsoft的依赖注入框架现在已可在您的项目中使用。您可以使用作用域、生命周期和模块构建更复杂的设置,请参阅官方文档。
源代码 (C#)using System.Collections; using System.Collections.Generic; using Unigine; using Microsoft.Extensions.DependencyInjection; [Component(PropertyGuid = "AUTOGENERATED_GUID")] public class ClientCode : Component { void Init() { var services = new ServiceCollection(); services.AddTransient<ISomeInterface, SomeImplementation>(); var serviceProvider = services.BuildServiceProvider(); var foo = serviceProvider.GetRequiredService<ISomeInterface>(); foo.Send("Hello, .NET DI!"); } }
本页面上的信息适用于 UNIGINE 2.20 SDK.