我正在尝试在 C# 中实现多态性,以便使用单个
ProcessEvent
方法处理各种事件类型。这是我当前设置的示例:
public class BaseProposedEvent
{
public required string EventType { get; set; }
}
public class ProposeTeamCreatedEvent : BaseProposedEvent
{
public required string TeamName { get; set; }
}
我想将派生类的实例(如
ProposeTeamCreatedEvent
)传递给ProcessEvent
,同时将参数声明为BaseProposedEvent
。但是,由于 TeamName
不是基类的属性,因此我无法在方法中直接访问它。例如:
public void ProcessEvent(BaseProposedEvent proposedEvent)
{
// I can't access `TeamName` if the event is not a `ProposeTeamCreatedEvent`.
}
我想到的一个解决方法是将 JSON 字符串和事件类型传递给
ProcessEvent
,但这种方法会变得混乱,因为它需要在服务器和客户端上进行序列化/反序列化。
在 TypeScript 中,这个问题可以通过类型联合和可区分联合轻松解决:
export type IncomingMessage =
| IAuthMessage
| IProposeEventMessage
| IProposeEndGameMessage
| IHeartBeatMessage;
export interface IAuthMessage {
type: "auth";
gameId: string;
userToken: string;
}
如何在 C# 中实现类似级别的灵活性,而无需求助于手动 JSON 处理?是否有更好的方法以干净且类型安全的方式处理多态事件类型?
另请注意,这是使用 SignalR 的应用程序的一部分。中心看起来像这样:
public async Task ProposeEvent(int gameId, string userToken, string eventType, BaseProposedEvent proposedEvent)
{
// todo: eventId should be set on client, no?
var gameState = gameStateService.GetGameState(gameId);
if (gameState is null)
{
logger.LogWarning("User {userToken} tried to join game {gameId}. The game does not exist", userToken, gameId);
throw new HubException("Game does not exist.");
}
if (!gameState.IsUserInGame(userToken))
{
logger.LogWarning("User {userToken} tried invoking JoinGame before joining via API", userToken);
throw new HubException("You must join the game via the API before connecting.");
}
if (!gameState.IsGameAdmin(userToken))
{
logger.LogWarning("User {userToken} is not the admin of game {gameId}. The user may not propose an event.", userToken, gameId);
throw new HubException("Only the admin may perform actions in this game.");
}
var processEventResult = await eventProcessor.ProcessEventAsync(proposedEvent, eventType, gameId, userToken, gameState);
if (processEventResult.IsFailure)
{
logger.LogCritical("The proposed event was invalid. Event type: {eventType}", eventType);
throw new HubException($"The proposed event was invalid. Event type: {eventType}.");
}
gameStateService.UpdateGameState(gameId, processEventResult.Value.UpdatedGameState);
await Clients.Group(GameUtility.GetGameIdString(gameId)).SendGameEvent(processEventResult.Value);
logger.LogInformation(
"State updated for game {gameId}. The event was processed successfully. Event type: {eventType}",
gameId,
eventType
);
}
```
I added `eventType` as an argument to `ProcessEvent` to help when narrowing the type of the `BaseEvent`.
假设添加
BaseProposedEvent.Process
方法来进行任何处理是不可行的,最简单的方法就是使用一个开关:
public void ProcessEvent(BaseProposedEvent proposedEvent)
{
switch (proposedEvent)
{
case ProposeTeamCreatedEvent proposeTeamCreatedEvent:
var teamName = proposeTeamCreatedEvent.TeamName;
break;
default: throw new ArgumentOutOfRangeException(nameof(proposedEvent));
}
}
此选项的缺点是您需要记住在添加新类型时添加新案例。一种替代方法是使用 访问者模式:
public interface IEventVisitor
{
public void Visit(ProposeTeamCreatedEvent obj);
}
public abstract class BaseProposedEvent
{
public string EventType { get; set; }
public abstract void Accept(IEventVisitor visitor);
}
public class ProposeTeamCreatedEvent : BaseProposedEvent
{
public string TeamName { get; set; }
public override void Accept(IEventVisitor visitor) => visitor.Visit(this);
}
public class ProcessVisitor : IEventVisitor
{
public void Visit(ProposeTeamCreatedEvent obj)
{
var teamName = obj.TeamName;
}
}
这或多或少做了相同的事情,但是每当您添加新的事件类型时,编译器都会强制您向
IEventVisitor
接口添加新方法,并更新其所有实现。因此忘记某事的风险要小得多。您可以考虑让访问者返回通用值,即 public T Visit(ProposeTeamCreatedEvent obj)
,以防访问者需要返回值。
您还可以在 c# 中实现可区分联合,例如,请参阅这篇关于 either monad 的文章。但如果您使用多态性并且需要处理任意数量的类型,我不确定这是否是正确的方法。