4

I have this example code. What I want to do is to make it so that the "Nums" value can only be written to using the "AddNum" method.

namespace ConsoleApplication1
{
    public class Person
    {
        string myName = "N/A";
        int myAge = 0;
        List<int> _nums = new List<int>();

        public List<int> Nums
        {
            get
            {
                return _nums;
            }
        }

        public void AddNum(int NumToAdd)
        {

            _nums.Add(NumToAdd);
        }

        public string Name { get; set; }
        public int Age { get; set; }
    }
}

Somehow, I've tried a bunch of things regarding AsReadOnly() and the readonly keyword, but I can't seem to get it to do what I want it to do.

Here is the sample of the code I have to access the property.

Person p1 = new Person();
p1.Nums.Add(25); //access 1
p1.AddNum(37); //access 2

Console.WriteLine("press any key");
Console.ReadLine();

I really want "access 1" to fail, and "access 2" to be the ONLY way that the value can be set. Thanks in advance for the help.

stumped221
  • 89
  • 4
  • 18

2 Answers2

11

√ DO use ReadOnlyCollection, a subclass of ReadOnlyCollection, or in rare cases IEnumerable for properties or return values representing read-only collections.

The quote from this article.

You should have something like this:

List<int> _nums = new List<int>();

public ReadOnlyCollection<int> Nums
{
    get
    {
        return _nums.AsReadOnly();
    }
}
Alex Sikilinda
  • 2,928
  • 18
  • 34
2

In general, collection types make poor properties because even when a collection is wrapped in ReadOnlyCollection, it's inherently unclear what:

IEnumerable<int> nums = myPerson.Nums;
myPerson.AddNum(23);
foreach(int i in nums) // Should the 23 be included!?
  ...

is supposed to mean. Is the object returned from Nums a snapshot of the numbers that existed when it called, is it a live view?

A cleaner approach is to have a method called something like GetNumsAsArray which returns a new array each time it's called; it may also be helpful in some cases to have a GetNumsAsList variant depending upon what the caller will want to do with the numbers. Some methods only work with arrays, and some only work with lists, so if only one of the above is provided some callers will have to call it and then convert the returned object to the required type.

If performance-sensitive callers will be needing to use this code a lot, it may be helpful to have a more general-purpose method:

int CopyNumsIntoArray(int sourceIndex, int reqCount, ref int[] dest, 
                      int destIndex, CopyCountMode mode);

where CopyCountMode indicates what the code should do the number of items available starting at sourceIndex is greater or less than reqCount; the method should either return the number of items that were available, or throw an exception if it violated the caller's stated expectations. Some callers might start by create and passing in a 10-item array but be prepared to have the method replace it with a bigger array if there are more than ten items to be returned; others might expect that there will be exactly 23 items and be unprepared to handle any other number. Using a parameter to specify the mode will allow one method to service many kinds of callers.

Although many collection authors don't bother including any method that fits the above pattern, such methods can greatly improve efficiency in cases where code wants to work with a significant minority of a collection (e.g. 1,000 items out of a collection of 50,000). In the absence of such methods, code wishing to work with such a range must either ask for a copy of the whole thing (very wasteful) or request thousands of items individually (also wasteful). Allowing the caller to supply the destination array would improve efficiency in the case where the same method makes many queries, especially if the destination array would be large enough to be put on the large object heap.

supercat
  • 77,689
  • 9
  • 166
  • 211
  • Hey supercat, I follow about 40% of what you are saying.. when I run the code you gave in my example: IEnumerable nums = myPerson.Nums; myPerson.AddNum(23); foreach(int i in nums) // Should the 23 be included!? ... the 23 is included are you saying it shouldn't be?.. or is that a question for me? – stumped221 Mar 18 '15 at 17:45
  • @stumped221: Some people looking just at the calling code (but unable to see the implementation of `Nums`) would expect the 23 to be included; some would expect that it wouldn't be included. A good API should--to the extent possible--be written so that people who look only at the calling code would be able to reasonably guess what it means, but there's no way a `Nums` property could behave that wouldn't surprise a significant fraction of people looking at the client-side code. By contrast, if code calls `GetNumsAsArray`, anyone seeing the method call would expect it to get a snapshot. – supercat Mar 18 '15 at 17:51
  • Hmmm.. can you give me an example of a scenario where someone instantiates an object, explicitly assigns or inserts a value to a property of that object then doesn't expect or want the value they just added to appear in their instance of that object? I'm kinda new to the OOP world, so I'm really just asking educationally. – stumped221 Mar 18 '15 at 17:56
  • @stumped221: The question is whether the caller expects `Nums` to be a live view of the object from which it was received, or represent a snapshot which is detached from it. To use an analogy, given `string foo=myControl.Text; myControl.Text+="*";` one would not expect the second statement to add an asterisk to `foo`, because `foo` represents a snapshot of what the control's `Text` property contained at the time it was called. – supercat Mar 18 '15 at 18:04
  • @stumped221: Suppose code wants to allow a user to edit the list of numbers, but provide an option to revert changes. One way to do that would be to capture a snapshot of the list before making changes, and restore the state of the list from the snapshot if the user selects "revert". That won't work, though, if what was supposed to be a snapshot is actually a live view. Sometimes callers will need a snapshot. Occasionally they might need a live view. Most of the time, callers will be actually be fine with either, *but* the nature of what they're getting should still be documented. – supercat Mar 18 '15 at 18:15
  • @stumped221: As a point of clarification, sometimes returning a property of a collection type may be the best way to allow client code to iterate things, but unless the type is intended to represent a live view it may be good to design it so that any changes to the collection will expressly invalidate the view object, so that any client code that expects it to be usable as a snapshot or live view will receive an exception rather than receiving data that may differ from expectations in ways the client doesn't realize. – supercat Mar 18 '15 at 18:24