比私密还要私密?(C#)

27
有时你会有一个私有字段,用来支持属性。你只想通过属性设置器来设置字段的值,以便在字段更改时执行额外的处理。问题是从同一类的其他方法内部意外绕过属性设置器仍然很容易,并且可能并未注意到这样做。在C#中有没有解决这个问题的办法或者避免这种情况的通用设计原则?

请参见 https://dev59.com/JnM_5IYBdhLWcg3ww2Kb - Ruben Bartelink
相关问题:在类中的哪些点上允许直接访问字段? - peterchen
解决方案是为您的私有字段使用适当的命名约定。 - Jon Grant
+1 这在处理 Azure 表实体时是必需的。通常,变量将反映 RowKey 或 PartitionKey 上的更改。 - TLDR
20个回答

41

我认为它不被使用,原因如下:

  • 类必须信任自己
  • 如果你的类变得非常庞大而其中一部分不知道另一部分,则应该将其拆分。
  • 如果属性背后的逻辑稍微复杂一些,请考虑将其封装在自己的类型中。

2
听起来不错,但这经常会影响一致性和简单的接口,或者需要大量的样板代码,即降低可维护性。 - peterchen
2
+1,@peterchen:你可以创建一个门面来替换这个类,从而调用被拆分成的所有其他类。 - Steven Evers
将属性封装到自己的类型中并不能帮助太多,正如JonB和Mehmet Aras在他们的答案中所展示的那样。 - HappyNomad
@HappyNomad。虽然JonB实际演示了它的工作原理,但我看不出问题所在。Mehmet只是指出问题存在于封装类中...当然,某些代码需要编写数据,否则该字段就没有任何用处。顺便说一句,我实际上没有像Mehmet建议的那样考虑一个人工类。我更多地考虑代表某物的类,例如Coordinate代替float[]Money代替decimalSerialized代替byte[]等。 - Stefan Steinegger

22
我认为这是一种不好的方法,如果可能的话应该避免使用,但是...
您可以将后备字段标记为过时,这样编译器在尝试访问它时会生成警告,然后在属性getter / setter中抑制该警告
需要抑制的警告代码是CS0612用于普通Obsolete属性,如果属性有自定义消息,则为CS0618
[Obsolete("Please don't touch the backing field!")]
private int _backingField;

public int YourProperty
{
    #pragma warning disable 612, 618
    get { return _backingField; }
    set { _backingField = value; }
    #pragma warning restore 612, 618
}

10
无意冒犯,但如果这个代码被审查发现了的话,程序员应该被请出来并开枪处决。不过,作为一个黑客技巧,它还是不错的 :) - Binary Worrier
3
完全同意@Binary Worrier的观点。我认为,对于这种问题,技术解决方案并不是真正的答案。真正的答案应该是“妥善培训开发人员,确保他们不会篡改自己不理解的代码”。 - LukeH
1
定义字段的可怕方式。我的意思是,看起来很有趣,但是谁会投票支持这种东西呢? - Stefan Steinegger
4
嗯,我想它(有点)回答了这个问题。另外注意,如果你使用Obsolete("message", true) C#将拒绝在#pragma块之外编译。 - Chris Chilvers
1
@KeeperOfTheSoul:如果你使用"Obsolete("Message", true)",那么C#将拒绝编译#pragma块内的内容。#pragma只会禁用警告,而不是错误。 - LukeH
显示剩余4条评论

20

目前没有内置的方法可以实现您想要的功能,但从事情的声音来看,您需要在类和该值之间加入另一层抽象。

创建一个单独的类,并将项目放入其中,然后您的外部类包含新类,只能通过其属性访问它。


11

不,这是不可能的。我自己非常希望有这样一个功能,类似于:

public string Name
{
    private string name; // Only accessible within the property
    get { return name; /* Extra processing here */ }
    set { name = value; /* Extra processing here */ }
}

我大约5年前在C#新闻组中首次提出了这个建议......但我不指望会实现它。

需要考虑序列化等方面的各种问题,但我仍然认为这很好。不过,我更希望首先实现自动实现的只读属性......


“自动实现只读属性”?像 public string Name { get; } 这样的?那会是怎样的工作方式呢? - Blorgbeard
不,它将是public readonly string Name { get; set; }。它的工作方式与普通的自动属性相同,但setter只能从构造函数中使用,就像普通的只读字段一样。 - Paul Batum
3
你只能在构造函数中给它赋值。我更喜欢使用 public string Name { get; private readonly set; } 这种语法,虽然有些冗长但意图更加清晰。 - ShuggyCoUk
@Jon,我很久之前就请求过这个功能,但是被告知它不够重要而无法提供。也许你给C#团队发一封邮件会有更好的结果。 - Ian Ringrose
@Ian:可能吧...虽然我可能已经向他们表达了这个愿望 :) (我仍然希望C# 5能够有只读自动实现属性...) - Jon Skeet

7

您可以通过在构造函数(或其他初始化函数)中使用局部变量的闭包来实现此操作。但是,与辅助类方法相比,这需要更多的工作。

class MyClass {
  private Func<Foo> reallyPrivateFieldGetter;
  private Action<Foo> reallyPrivateFieldSetter;
  private Foo ReallyPrivateBackingFieldProperty {
    get { return reallyPrivateFieldGetter(); }
    set { reallyPrivateFieldSetter(value); }
  }

  public MyClass() {
    Foo reallyPrivateField = 0;
    reallyPrivateFieldGetter = () => { return reallyPrivateField; }
    reallyPrivateFieldSetter = v => { reallyPrivateField = v; };
  }
}

我怀疑底层字段类型 Foo 需要是一个引用类,这样两个闭包才能在同一个对象上创建。


啊,我想贴上我自己的这个特定解决方案版本,结果发现你已经抢先了!+1 - Paul Batum

5
你可以将所有的私有字段放入一个嵌套类中,并通过公共属性进行暴露。然后在你的类内部,实例化该嵌套类并使用它。这样,那些私有字段将不会像它们本来属于主类时那样可访问。
public class A
{
   class FieldsForA
   {
      private int number;
      public int Number
      {
         get
         {
           //TODO: Extra logic.
           return number;
         }
         set
         {
           //TODO: Extra logic.
           number = value;
         }
      }
   } 
   FieldsForA fields = new FieldsForA();

   public int Number
   {
      get{ return fields.Number;}
      set{ fields.Number = value;}
   }       
}

它只提供了一定程度的障碍。访问私有后备字段的根本问题仍然存在于嵌套类中。但是,类A内部的代码无法访问FieldForA的私有字段,必须通过公共属性进行访问。


5

在C#中没有这样的规定。

然而,我会给私有变量取不同的名字(例如 m_something 或只是 _something),这样在使用时更容易找到它。


私有变量的下划线前缀是如此常见,以至于如果它被用于“更私有”的变量,我会感到惊讶。对于那些新接触这个代码库的人来说,应该使用一些更加笨拙的东西来引起他们的注意。 - Malte Clasen

3
也许可以使用属性备份存储,类似于 WPF 存储属性的方式
因此,您可以拥有:
Dictionary<string,object> mPropertyBackingStore = new Dictionary<string,object> ();

public PropertyThing MyPropertyThing
{
    get { return mPropertyBackingStore["MyPropertyThing"] as PropertyThing; }
    set { mPropertyBackingStore["MyPropertyThing"] = value; }
}

现在您可以进行所有的预处理,放心使用属性访问器相比直接访问变量,后者要困难得多。
附注:您甚至可以使用WPF的依赖属性基础设施...
此外,这显然会产生转换成本,但这取决于您的需求 - 如果性能很关键,也许这不是您的解决方案。
别忘了初始化备份存储!(;
编辑:
实际上,如果您将存储的值属性更改为属性存储对象(例如使用命令模式),则可以在命令对象中进行处理...只是一个想法。

3
无法在标准C#中完成此操作,但您可以:
  • define a custom attribute say OnlyAccessFromProperty

  • write your code like

    [OnlyAccessFromProperty(Name)]
    String name
    
    Name 
    {
       get{return name;}
    }
    
    etc …
    
  • Then write a custom rule for FxCop (or another checker)

  • Add FxCop to your build system so if your custom rule find an error the build is failed.

我们是否需要一组标准的自定义规则/属性来强制执行常见的设计模式,而无需扩展C#?


2

C#没有这个语言特性。但是,您可以依靠命名约定,类似于根本没有私有属性的语言。使用_p_前缀来命名更私有的变量名称,这样您就可以确信不会意外输入它。


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