C# 只读对象

30

有没有办法返回一个对象的只读实例?

public class Person
{
    public String FirstName { get; set; }
    public String LastName { get; set; }
}

public class SomeClass
{
    public SomeClass(Person manager)
    {
        if (manager == null)
            throw new ArgumentNullException("manager");

        _manager = manager;
    }

    private readonly Person _manager;
    public Person Manager
    {
        get { return _manager; } //How do I make it readonly period!
    }
}
这个问题的唯一解决方法是返回一个Clone(),以便对Clone进行任何更改而不影响原始对象。我知道对于数组,有一个函数可以将其作为只读返回。噢,我知道这是一个引用类型……我更想知道是否有一些隐藏的C#功能来锁定写入部分。
我试图想出一个通用的ReadOnly包装类,但无法在不进行一些昂贵的反射等操作的情况下将属性设置为只读。噢,我真的很想避免创建第二个所有属性都是只读的版本的类。此时,我不如返回Clone。

1
根据评论:是否有已知的“只读”通用包装类,快速高效?我尝试制作的那个必须使用反射,这并不是我想要的,如果可以避免的话。 - myermian
8个回答

43

为了避免创建额外的类,您可以将其实现为一个只有只读属性的接口IPerson。

public interface IPerson
{
    string FirstName { get; }
    string LastName { get; }
}
public class Person:IPerson
{
    public String FirstName { get; set; }
    public String LastName { get; set; }
}

public class SomeClass
{
    public SomeClass(Person manager)
    {
        if (manager == null)
            throw new ArgumentNullException("manager");

        _manager = manager;
    }

    private readonly Person _manager;
    public IPerson Manager
    {
        get { return _manager; } //How do I make it readonly period!
    }
}

1
使用接口而不是具体类型有很好的理由(如果Person有任何行为,我也建议这样做)...但与创建类相比,这样做如何节省任何努力? - Jeff Sternal
7
我也喜欢这样做,但他们不是可以很容易地将其转换回Person类吗?Person iCanChangeProperties = (Person)SomeClass.Manager;而且强制类型转换并不能防止他们使用该引用来更改属性...我可能只能坚持使用克隆。不过,只读接口很好用,这样反射会告诉他们他们不能对setter属性进行更改。 - myermian
5
@myermian,你说得对。虽然将其强制转换为Person是用户的不良实践。将Person定义为internal可以阻止这种行为(但会防止他们构建新实例)。即使标记为internal,在使用反射时用户也可以获取到setter的访问权限。同时,返回一个克隆对象也无法防止用户反射SomeClass并直接获取*_manager*。你无法阻止用户自寻烦恼。一旦有人开始违反公开API,他们应该承担任何由此带来的后果。 - statenjason
1
@jeff 我认为好处可以从维护Person中获得。 如果IPerson接口发生更改,编译器将强制更新Person以匹配它。 此外,如果使用了ReadOnlyPerson类,则需要创建某种Person->ReadOnlyPerson的映射。 - statenjason
@statenjason,我想你是对的。你只能在一定程度上保护用户免受“自食其果”的伤害。 - myermian

9
您可以将Person类转换为不可变对象,方法如下...
public class Person 
{ 
    public Person( string firstName, string lastName )
    {
        FirstName = firstName;
        LastName = lastName;
    }

    public String FirstName { get; private set; } 
    public String LastName { get; private set; } 

} 

4

以下是一个基于.NET Framework 2.0实现的List.AsReadOnly示例,其中使用布尔值(IsReadOnly)在适当的方法中防止更新:

public class Person
{
    private string _firstName;
    public string FirstName
    {
        get { return _firstName; }
        set
        {
            if (!IsReadOnly) _firstName = value;
            else throw new AccessViolationException("Object is read-only.");
        }
    }

    private string _lastName;
    public string LastName
    {
        get { return _lastName; }
        set
        {
            if (!IsReadOnly) _lastName = value;
            else throw new AccessViolationException("Object is read-only.");
        }
    }

    internal virtual bool IsReadOnly { get { return false; } }

    public ReadOnlyPerson AsReadOnly()
    {
        return new ReadOnlyPerson(this);
    }

    public class ReadOnlyPerson : Person
    {
        private Person _person;
        internal override bool IsReadOnly { get { return true; } }

        internal ReadOnlyPerson(Person person) // Contructor
        {
            this._person = person;
        }
    }
}

测试它:

static void Main(string[] args)
{
    Person p1 = new Person();
    p1.FirstName = "Joe";
    p1.LastName = "Bloe";
    Console.WriteLine("First = {0} Last = {1}", p.FirstName, p.LastName);

    var p2 = p1.AsReadOnly();
    p2.FirstName = "Josephine"; // AccessViolationException
}

4

通过使用 Castle.DynamicProxy,您可以在某些条件下冻结对象(使其无法更改)。 有关详细信息,请阅读此博客文章


2

没有这样的功能 - 你已经覆盖了你的选项。

要么克隆它,要么创建一个只读的Person类型。后一种方法通常更受欢迎,因为语义更清晰:调用者很明显不应该(也不能)修改实例。


这种方法在C#标准库中也很常见,例如String/StringBuilderUri/UriBuilder等。只有当只读用法远远超过可写用法时才有意义,而这两者正是如此。 - Liz Av

1

没有一种方法可以从类外部使对象的所有属性都只读。在上面的示例中,除非您将Person类中的属性更改为只读,否则无法使_manager属性只读。

您可以将Person类的属性setter设置为internal,这意味着只有与Person相同程序集中的类才能更改属性。

或者,如果将属性的setter设置为private,则只有Person内部的代码才能更改属性的值。


0

不是的。您正在寻找类似于C ++样式的const-ness,由于各种原因,C#没有这个。

但是,匿名类型是真正不可变的。


由于链接似乎已损坏,如果您记得的话,可以详细说明这些原因吗? - pooya13
1
@pooya13 我更新了链接到archive.org的版本。 - Craig Stuntz

0

匿名对象是只读的。


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