关于C#:如何处理因实现类而异的静态字段

关于C#:如何处理因实现类而异的静态字段

How to handle static fields that vary by implementing class

我一直都遇到这个问题。假设我正在创建命令行界面(Java或C#,问题与我想的相同,我将在此处显示C#)。

  • 我定义一个接口ICommand
  • 我创建了一个抽象基类CommandBase,它实现了ICommand,以包含通用代码。
  • 我创建了几个实现类,每个实现类都扩展了基类(并通过扩展了接口)。
  • 现在-假设接口指定所有命令都实现Name属性和Execute方法...

    对于Name,我的每个实例类都必须返回一个字符串,该字符串是该命令的名称。该字符串(" HELP"," PRINT"等)对于相关类是静态的。我希望能够做的是定义:

    公共抽象静态常量字符串名称;

    但是(很遗憾)您不能在接口中定义静态成员。

    我多年来一直在为这个问题而苦苦挣扎(几乎在我拥有类似课程的家庭中任何地方),因此下面将发表我自己的3种可能的解决方案,供您投票。但是,由于它们都不是理想的,我希望有人能发布一个更优雅的解决方案。

    更新:

  • 我无法使代码格式正常工作(Safari / Mac?)。道歉。
  • 我使用的示例很简单。在现实生活中,有时会有数十个实现类和这种半静态类型的多个字段(即实现类为静态)。

  • 我忘了提-理想情况下,我希望能够静态查询此信息:

    字符串名称= CommandHelp.Name;

  • 在我提出的3个解决方案中,有2个要求实例化该类,然后才能发现此静态信息,这很丑陋。


    正如您提到的,没有办法从接口级别强制执行此操作。但是,由于您使用的是抽象类,因此您可以在基类中将属性声明为abstract,这将强制继承类覆盖它。在C#中,它将如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public abstract class MyBaseClass
    {
      public abstract string Name { get; protected set; }
    }

    public class MyClass : MyBaseClass
    {
      public override string Name
      {
        get { return"CommandName"; }
        protected set { }
      }
    }

    (请注意,受保护的集可防止外部代码更改名称。)

    这可能不完全是您要找的东西,但它与我想的差不多。根据定义,静态字段不变。对于给定的类,您根本无法拥有既静态又可重写的成员。


    您可能会考虑使用属性而不是字段。

    1
    2
    3
    4
    [Command("HELP")]
    class HelpCommand : ICommand
    {
    }


    1
    2
    3
    4
    5
    6
    7
    8
    9
    public interface ICommand {
          String getName();
    }

    public class RealCommand implements ICommand {
         public String getName() {
               return"name";
         }
    }

    就那么简单。为什么要麻烦一个静态场?

    观察:不要在抽象类中使用应在子类中初始化的字段(如David B建议)。如果有人扩展了抽象类而忘记了初始化字段怎么办?


    像这样的事情呢:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    abstract class Command {
       abstract CommandInfo getInfo();
    }

    class CommandInfo {
      string Name;
      string Description;
      Foo Bar;
    }

    class RunCommand {
      static CommandInfo Info = new CommandInfo() { Name ="Run", Foo = new Foo(42)  };

      override commandInfo getInfo() { return Info; }
    }

    现在,您可以静态访问信息:

    1
    RunCommand.Info.Name;

    从您的基类:

    1
    getInfo().Name;

    从设计的角度来看,我认为需要一个静态的实现成员是错误的。在性能和内存使用之间的相对差异是静态的,而不是示例字符串。除此之外,我知道在实施中,所涉及的对象可能具有明显更大的占地面积...

    根本的问题是,通过尝试建立模型以支持在C#的基础或接口级别可用的静态实现成员,这是我们的选项受到限制的。在接口级别仅提供属性和方法。

    下一个设计难题是代码??是基础代码还是特定于实现的代码。通过实现,您的模型将在编译时得到验证,该代码必须在所有实现中都包含类似的逻辑。使用base时,您的检定将在运行时发生,但是逻辑将集中在一个地方。不幸的是,由于没有逻辑与数据相关联,因此给出的示例是针对特定执行代码的完美展示。

    因此,出于示例的考虑,假设存在与数据相关联的某些实际逻辑,并且该逻辑足够广泛/复杂,足以为基础分类提供展示。撇开基类逻辑是否使用任何实现细节,我们要确保实现静态初始化。我建议在基类中使用受保护的抽象,以强制所有实现创建所需的静态数据,这些数据将在编译时进行评估。我使用的所有IDE都可以使此操作变得非常容易。对于Visual Studio,只需单击几下鼠标,然后实质上更改返回值。

    回到问题的非常具体的性质,而忽略许多其他设计问题……如果您真的必须将整个问题保持在静态数据的本质上,并且仍然通过问题的本质加以实施,那么……一定要去通过属性的方法,因为有很多副作用可以利用属性。在基类上使用静态成员,在实现上使用静态构造函数来设置名称。现在请记住,您必须在运行时而不是编译时验证名称。基本上,基类上的GetName方法需要处理实现未设置其名称时发生的情况。它可能会引发异常,从而使人们很残酷地意识到,某种实现会由于测试/ QA而不是用户的原因而感到厌倦。或者,您可以使用反射来获取实现名称并尝试生成一个名称...反射的问题在于,它可能会影响子类并设置代码环境,这对于初级开发人员而言很难理解和维护。 ..

    为此,您始终可以通过反射从类名生成名称...尽管从长远来看,这可能是一个噩梦,但是它会减少实现上所需的代码量,这似乎更重要比其他任何问题都重要。您也可以在此处使用属性,但是随后您将在时间/工作量上等同于静态构造函数的代码添加到实现中,并且在实现不包含该信息时该怎么办仍然存在问题。


    [建议的答案#3 of 3]

    我还没有尝试过,并且在Java中还不是很好(我认为呢?),但是我可以用Attributes标记类:

    [CammandAttribute(名称="HELP")]

    然后,我可以使用反射来获取该静态信息。需要一些简单的帮助程序方法,以使信息可以轻松地供类的客户端使用,但这可以在基类中使用。


    我的答案将与Java有关,因为这就是我所知道的。接口描述行为,而不是实现。此外,静态字段与类(而不是实例)相关。如果您声明以下内容:

    1
    2
    3
    interface A { abstract static NAME }
    class B { NAME ="HELP" }
    class C { NAME ="PRINT" }

    然后此代码如何知道要链接到哪个NAME:

    1
    2
    3
    void test(A a) {
        a.NAME;
    }

    我建议如何实现这一点,是以下方法之一:

    • 类名约定,基类从类名派生名称。如果您希望与此不同,请直接覆盖该接口。
    • 基类有一个带有名称的构造函数
    • 使用批注并通过基类强制其存在。

    但是,一个更好的解决方案可能是使用枚举:

    1
    2
    3
    4
    public enum Command {
        HELP { execute() }, PRINT { execute() };
        abstract void execute();
    }

    这更加干净,并且允许您使用switch语句,并且很容易获得NAME。但是,您不能扩展选项运行时的数量,但是可以根据您的方案描述来扩展甚至不需要。


    [建议的解决方案#2,共3]

    • 设置一个私有成员变量名称。
    • 在接口中定义一个抽象属性Name。
    • 像这样在基类中实现属性:

      1
      2
      3
      4
      public string Name
      {
        get {return Name;}
      }
    • 强制所有实现在调用抽象基类构造函数时将名称作为构造函数参数传递:

      1
      2
      3
      4
      public abstract class CommandBase(string commandName) : ICommand
      {
        name = commandName;
      }
    • 现在,我所有的实现都在构造函数中设置了名称:

      1
      public class CommandHelp : CommandBase(COMMAND_NAME) {}

    好处:

    • 我的访问器代码集中在基类中。
    • 名称定义为常量

    缺点

    • Name现在是一个实例变量-
      我的Command类的每个实例
      进行新的引用而不是
      共享静态变量。

    我通常做的事(伪):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    abstract class:

    private const string nameConstant ="ABSTRACT";

    public string Name
    {
       get {return this.GetName();}
    }

    protected virtual string GetName()
    {
       return MyAbstractClass.nameConstant;
    }

    ----

    class ChildClass : MyAbstractClass
    {
       private const string nameConstant ="ChildClass";

       protected override string GetName()
       {
          return ChildClass.nameConstant;
       }
    }

    当然,如果这是其他开发人员将使用的库,则在属性中添加一些反射以验证当前实例是否确实实现了覆盖或抛出异常" Not Implemented"也不会受到损害。


    只需将name属性添加到基类中,然后将其传递给基类的构造函数,并将派生类的构造函数传递给它的命令名称


    [建议的解决方案#1,共3]

  • 在接口中定义一个抽象属性Name,以强制所有实现类实现name属性。
  • (在c#中)将此属性作为抽象添加到基类中。
  • 在这样的实现中实现:

    1
    2
    3
    4
    public string Name
    {
      get {return COMMAND_NAME;}
    }

    其中name是该类中定义的常量。

    好处:

    • 名称本身定义为常量。
    • 接口要求创建属性。

    缺点:

    • 复制(我讨厌)。粘贴到我的每个实现中的属性访问器代码都完全相同。为什么不能在基类中这样做以避免混乱?

  • 推荐阅读