如何禁止使用字段而不是属性?

13

我在我的类中有一个属性,在set访问器中具有很多逻辑:

private String text;

public String Text
{
   get { return text; }
   private set
   {
      // some actions with a value
      value = value.Replace('a', 'b');

      text = value;
   }
}

如何防止其他开发人员(甚至是我自己)在这个类中更改 field 而不是 property

如果有人写出这样的代码:

public Test(String text)
{
   this.text = text;
}

它会破坏我的类的逻辑!

5个回答

19

通常,类应该足够小,以至于这不应该是一个问题。由于该字段是private的,只有类型内部的代码才能访问该字段。但是,如果您需要强调其重要性,可以执行以下操作:

[Obsolete("Use the Text property, or I break your legs; fair warning")]
private string text;

public string Text
{
#pragma warning disable 0618
    get { return text; }
    private set
    {
        // some actions with a value
        value = value.Replace('a', 'b');

        text = value;
    }
#pragma warning restore 0618
}

这不会阻止它们,但可能有助于防止意外使用该字段。


5
“否则我会打断你的腿(或者我的)。”(他想要防止自己受伤) - Raphaël Althaus
2
@edencore 顺便说一下:你可能可以使用 Roslyn 实现一个精确的“查找所有非法字段使用并警告它们”的功能——问题在于,如何定义“非法字段使用”和“合法字段使用”。 - Marc Gravell
另一种同样丑陋的方法是重命名字段,例如 private string textNoSanitationWhenWrittenToDontUseWeWillBreakYourLegs; - Jeppe Stig Nielsen
1
@MarcGravell:确实,有一个函数set是特权函数,因为它被允许修改text,而其他函数则不行。另外一个函数get被允许读取但不能写入。这就是要捕捉的概念,但我不知道在静态代码分析方面表达这一点的现状。当然,除了Binary Worrier的答案之外,他已经用编译器已经理解的语言清晰地表达了这个概念。 - Steve Jessop
1
@BinaryWorrier 我点击了那个链接,希望能得到一个解释,而不仅仅是“你应该被枪毙”... - Rawling
显示剩余5条评论

9

有一种正确的方法来做这件事。考虑一下古老的格言“计算机科学中的所有问题都可以通过引入另一个间接层次来解决”。

你需要创建一个类,它知道如何正确设置文本字段的值。

class TextSetter
{
    private string text;
    public TextSetter(string text)
    {
        Text = text;
    }
    public string Text
    {
        get{ return text;}
        set{ text = value.Replace('a', 'b');}
    }
}

在您的第一个类中,不要使用private string text;而是使用private TextSetter text。现在没有人会意外地直接设置该值了。
如果这是一个常见问题,甚至可以将其泛化。
class FieldSetter<T>
{
    private T field;
    private Func<T, T> setter;
    public FieldSetter(Func<T, T> setter, T field)
    {
        this.setter = setter
        Value = field;
    }
    public T Value
    {
        get{ return field;}
        set{ field = setter(value);}
    }
}

如您所愿,随意更改名称。


虽然在分配方面不太好。 - Marc Gravell
这是真的,但它只是另一个分配而已。如果类上再进行一次分配就会让你心碎,那么你可以将该类设置为结构体,然后你就可以针对Setter和字符串引用“text”进行单个分配。99%的时间,对于软件开发人员编写的99%的软件来说,额外的分配比保持代码意图诚实更不重要。我们并不都遭受GC的攻击,我们中很少有人处理StackOverflow类型的数据量:p - Binary Worrier
有一个明显的警告,即您需要正确实现结构体(即作为不可变类型),这意味着您每次分配都需要进行一次分配...也许不要将类作为结构体。 - Binary Worrier
好的,谢谢伙计 :) - Binary Worrier
我添加了一个单独的答案,以更完整地说明“struct”的使用 - Marc Gravell
显示剩余3条评论

6

作为替代方法,基于与Binary Worrier的讨论(评论)链接,你可以采取类似以下方式:

internal struct SanitizedText {
    private readonly string value;
    private SanitizedText(string value) {
        this.value = value;
    }
    public static implicit operator string (SanitizedText value) {
        return value.value;
    }
    public static implicit operator SanitizedText(string value) {
        if (value != null) value = value.Replace('a', 'b');
        return new SanitizedText(value);
    }
}

然后简单地执行:
private SanitizedText text;
public string Text {
    get { return text; }
    set { text = value; }
}

这段代码对于所有的验证都是一样的,但它不可能被滥用:您无法创建一个绕过验证的SanitizedText值;同样地,它不需要更多的空间(struct的大小是内容的大小:1个引用),也不需要分配。它甚至以相同的方式默认为null字符串。


+1 给你!(额外字符) - Binary Worrier
如果有一种方法可以让类型包含一个“渴望”提升运算符,那么我会喜欢这种方法,以便如果编译会因为某种类型“原样”而失败,编译器应该看看它被急切地推广后会发生什么,以允许 someSanitizedString.LengthsomeOtherTypeConvertibleFromString = someSanitizedString; 而不需要使用 ((string)someSanitizedString).LengthsomeOtherTypeConvertibleFromString = (string)someSanitizedString;。 如果没有这样的功能,试图使类型像内置类型一样行事最终会创建非常泄漏的抽象。 - supercat
@supercat(晚评论):既然属性 Text(你唯一应该使用的)返回 string,那么 Text.Length 就没问题了。如果你使用 someSanitizedString.Length,那么你就在使用支持字段,而这是被防止的。 - NetMage
@NetMage:我的意思是,如果能够像使用字符串一样使用字符串类型的字段,而不必手动访问“Text”属性,那将非常有用。 - supercat
除非尝试以需要“字符串”的方式使用“ConcatenatedString”,否则不需要创建任何实际的“String[]”对象。 - supercat
显示剩余2条评论

5
你已经通过将字段的修饰符设置为private来做到了最好。在类内部,你通常应该知道如何处理其字段以及逻辑发生的位置。

1
你可以使用集合(每种类型一个)作为“后备存储”:
private static readonly Dictionary<string, int> __intFields = new Dictionary<string, int>();
private static readonly Dictionary<string, string> __stringFields = new Dictionary<string, string>();

public static string StringProp
{
    get
    { 
        var fieldValue = default(string);
        __stringFields .TryGetValue("StringProp", out fieldValue);
        return fieldValue;
    }
    set
    {
        var manipulatedValue = applyOtherCode(value); // the rest of the code
        __stringFields ["StringProp"] = manipulatedValue
    }
}

public static int IntProp
{
    get
    { 
        var fieldValue = default(int);
        __intFields .TryGetValue("IntProp", out fieldValue);
        return fieldValue;
    }
    set
    {
        var manipulatedValue = applyOtherCode(value); // the rest of the code
        __intFields ["IntProp"] = manipulatedValue
    }
}
// There is no field for anyone to mistakenly access!

丑陋,冗长,不可重构...但它能完成工作。如果性能不是您最关心的问题(即使它是一个问题),您可以通过反射获取属性名称并避免使用魔术字符串。

我最初有一个单一的<string,object>集合...但我讨厌它。每种类型一个集合可以避免频繁转换数据


丑陋,是的。在getter中,您应该考虑在字典上使用TryGetValue以避免两次查找键。在setter中,您可以只说__backingFields["Prop"] = manipulatedValue;而不需要if,因为它将根据需要替换或添加。该字段可以设置为readonly。在这些更改之后,它仍然非常丑陋,特别是由于从objectstring的转换。 - Jeppe Stig Nielsen
优化完毕。我也讨厌那个字符串转对象的强制转换,所以我改变了代码,使其每种类型只有一个字典(这看起来像是适合放在codegolf上...)。干杯。 - Alex

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接