C# - 映射输入对象以类型安全的方式影响对象

C# - Mapping input objects to effect objects in a typesafe way

提问人:stackedAE 提问时间:12/2/2020 更新时间:12/9/2020 访问量:195

问:

我试图解决的问题在概念上非常简单。我将用游戏来解释它,但我相信这个概念适用于任何具有输入、效果关系的对象:

技能由输入对象列表和效果对象列表组成。输入对象定义用户交互的类型(如选择地面图块或选择单位),每个输入对象将从用户那里获得某种类型的目标(例如图块或单位)。效果对象定义对特定类型目标的效果(例如,移动到地面图块或损坏单位)。

在代码中使用能力包括:

  • 按顺序提示用户输入对象(等待用户完成每个对象的输入,然后再继续下一个)
  • 按顺序执行效果(每个效果将从映射到的输入对象获取数据)

因此,定义能力的一个例子可能是:

Ability: {
   Inputs: {
       Select Tile,
       Select Unit
   }

   Effects: {
       Move to (input 1),
       Deal 10 damage (input 2)
   }
}

理想情况下,我想使从输入到效果类型的映射是安全的,因为每种类型的效果都有它期望的目标数据类型(例如图块或单元)。 下面是我编写的代码示例,用于表示代码中的操作、输入和效果对象:

    public class AbilityData
    {
        List<InputData<Target>> inputs;

        List<AbilityEffect> effects;

        public void exampleCreate()
        {
            BaseInputData<Tile> input = new TileSelectData();
            inputs.Add(input);

            effect = new List<AbilityEffect>();
            BaseAbilityEffect<Tile> effect = new BaseAbilityEffect<Tile>(new TileEffect(), input);
            effects.Add(effect);
        }
        public void exampleRun()
        {
            foreach (InputData<AbilityInput> input in inputs)
            {
                input.promptInput();
            }

            foreach (AbilityEffect effect in effects)
            {
                effect.execute();
            }
        }
    }
   public interface AbilityEffect
    {
        void execute();
    }

    public class BaseAbilityEffect<T> : AbilityEffect where T : Target
    {
        InputData<T> input;

        TargetEffect<T> effect;

        public BaseAbilityEffect(TargetEffect<T> tEffect, InputData<T> input) {
            this.input = input;
            this.effect = tEffect;
        }

        public void execute()
        {
            effect.execute(input.getInput());
        }
    }

    public class TargetEffect<T> where T : Target
    {
        public virtual void execute(T input) { }
    }

    public class UnitEffect : TargetEffect<Unit>
    {
        public override void execute(Unit unit)
        {
            // Do something on the unit
        }
    }

    public class TileEffect : TargetEffect<Tile>
    {
        public override void execute(Tile tile)
        {
            // Do something on the tile
        }
    }

    public interface InputData<out T>
    {
        void promptInput();

        T getInput();
    }

    public abstract class BaseInputData<T> : InputData<T> where T : Target
    {
        public abstract T getInput();

        public abstract void promptInput();
    }

    public class TileSelectData : BaseInputData<Tile>
    {
        public override Tile getInput()
        {
            // Return the stored input
        }

        public override void promptInput()
        {
            // prompt for the input and store it.
        }
    }

    public class UnitSelectData : BaseInputData<Unit>
    {
        public override Unit getInput()
        {
            // Return the stored input
        }

        public override void promptInput()
        {
            // prompt for the input and store it.
        }
    }

这似乎在正常的本地情况下可以正常工作,但是当您需要为输入提供覆盖时,问题就出现了。例如,在联网游戏中,客户端将触发输入,然后将目标发送到主服务器。然后,主服务器需要覆盖输入对象以使其接收到目标,然后调用效果。

所以我想添加类似的东西

void overrideInput(T);

添加到 InputData 接口,但由于它使用协方差,因此我不能将泛型类型参数用作任何接口函数的参数。

有没有办法绕过这个限制?这个概念看起来非常简单:确保效果对象仅与相同目标类型的输入对象匹配。当然,我可以通过一堆不安全的铸件以蛮力完成此任务,但似乎应该有更好的方法。

C# 泛型强制 转换 协方差 类型安全

评论

0赞 chtenb 12/12/2020
在您的示例中,第一个效果作用于第一个输入,第二个效果作用于第二个输入。情况总是这样吗?
0赞 stackedAE 12/13/2020
不一定。例如,你可以有一个技能,只有一个输入,但有多个效果(例如输入:选择一个单位,效果:1)对单位造成伤害,2)眩晕单位)。每个效果都只与一个输入相关联(当前传递到 BaseAbilityEffect 构造函数中),并作用于该输入。
0赞 chtenb 12/13/2020
我发现命名有点不直观,这使得概念化事物应该如何运作变得更加困难。当然,我没有完整的上下文,但我认为会将输入重命名为主题或其他东西。
0赞 chtenb 12/13/2020
令我感到奇怪的一件事是,效果和输入存储在单独的列表中,但它们应该以某种方式以类型安全的方式匹配在一起。
0赞 chtenb 12/13/2020
为了使您的问题和要求更易于理解,您应该给出有关如何使用此代码的示例。Typesafety 始终只在“客户端代码”方面有意义。如果客户端代码是完全多态的,则 typesafety 的实际安全性为零。

答:

0赞 Siderite Zackwehdex 12/9/2020 #1

我通过添加另一种类型 Tinput 来编译您的代码。我不知道这是否是你想要的:

    public class AbilityData
    {
        List<InputData<Target,Target>> inputs;

        List<AbilityEffect> effects;

        public void exampleCreate()
        {
            BaseInputData<Tile,Target> input = new TileSelectData();
            inputs.Add(input);

            var effects = new List<AbilityEffect>();
            var effect = new BaseAbilityEffect<Tile,Target>(new TileEffect(), input);
            effects.Add(effect);
        }
        public void exampleRun()
        {
            foreach (InputData<AbilityInput, AbilityInput> input in inputs)
            {
                input.promptInput();
            }

            foreach (AbilityEffect effect in effects)
            {
                effect.execute();
            }
        }
    }
    public interface AbilityEffect
    {
        void execute();
    }

    public class BaseAbilityEffect<T,Tinput> : AbilityEffect 
        where T : Target,Tinput
    {
        InputData<T,Tinput> input;

        TargetEffect<T> effect;

        public BaseAbilityEffect(TargetEffect<T> tEffect, InputData<T,Tinput> input)
        {
            this.input = input;
            this.effect = tEffect;
        }

        public void execute()
        {
            effect.execute(input.getInput());
        }
    }

    public class TargetEffect<T> where T : Target
    {
        public virtual void execute(T input) { }
    }

    public class UnitEffect : TargetEffect<Unit>
    {
        public override void execute(Unit unit)
        {
            // Do something on the unit
        }
    }

    public class TileEffect : TargetEffect<Tile>
    {
        public override void execute(Tile tile)
        {
            // Do something on the tile
        }
    }

    public interface InputData<out T,Tinput>
        where T: Tinput
    {
        void promptInput();

        T getInput();

        void overrideInput(Tinput input);
    }

    public class Target
    {
    }

    public class Tile : Target
    {
    }

    public class TileSelectData : BaseInputData<Tile,Target>
    {
        public TileSelectData()
        {
        }
    }

    public class Unit : Target
    {
    }

    public class AbilityInput
    {
    }

    public class BaseInputData<T,Tinput> : InputData<T,Tinput>
        where T:Tinput
    {
        public T getInput()
        {
            throw new NotImplementedException();
        }

        public void overrideInput(Tinput input)
        {
            throw new NotImplementedException();
        }

        public void promptInput()
        {
            throw new NotImplementedException();
        }
    }

评论

0赞 stackedAE 12/9/2020
对不起,代码被修改以使其更通用和易于理解(编译的原始代码)。更改的问题在于,现在在 InputData 中,您必须定义两种类型,out T 和 Tinput,但实际上它们应该始终是相同的类型(InputData 只处理一种类型的 Target)。