84

The point of const-correctness is to be able to provide a view of an instance that can't be altered or deleted by the user. The compiler supports this by pointing out when you break constness from within a const function, or try to use a non-const function of a const object. So without copying the const approach, is there a methodology I can use in C# that has the same ends?

I'm aware of immutability, but that doesn't really carry over to container objects to name but one example.

TylerH
  • 20,799
  • 66
  • 75
  • 101
tenpn
  • 4,556
  • 5
  • 43
  • 63
  • There's a nice discussion about possible designs for readonly at [http://blogs.msdn.com/brada/archive/2004/02/04/67859.aspx](http://blogs.msdn.com/brada/archive/2004/02/04/67859.aspx) – Asaf R Sep 22 '08 at 10:37
  • Then what about implementing `const_cast` in C#? – kerem Sep 11 '12 at 07:13
  • Related post - [Const function parameter in C#](https://stackoverflow.com/q/10981888/465053) – RBT Jan 13 '22 at 06:20

7 Answers7

64

I've come across this issue a lot of times too and ended up using interfaces.

I think it's important to drop the idea that C# is any form, or even an evolution of C++. They're two different languages that share almost the same syntax.

I usually express 'const correctness' in C# by defining a read-only view of a class:

public interface IReadOnlyCustomer
{
    String Name { get; }
    int Age { get; }
}

public class Customer : IReadOnlyCustomer
{
    private string m_name;
    private int m_age;

    public string Name
    {
        get { return m_name; }
        set { m_name = value; }
    }

    public int Age
    {
        get { return m_age; }
        set { m_age = value; }
    }
}
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Trap
  • 12,050
  • 15
  • 55
  • 67
  • 3
    This isn't one of those questions that has an "answer" as such, but I really like this solution. It's elegant, can be made to fit a lot of situations, and is quick to write. – tenpn Sep 22 '08 at 20:14
  • 15
    Sure, but what if one of your fields is a List, or a rich type. Your solution gets complicated very quickly. – Matt Cruikshank Oct 17 '08 at 21:11
  • Returning an internal collection is considered a bad practice. You either return a read-only interface to manage that particular read-only list or a copy of the original list so no harm can be done. – Trap Nov 04 '08 at 16:12
  • 14
    @Trap: that is the whole point of the question. Returning an internal collection is a bad practice because the outside world could modify your internals, in C++ that is solved with the use of const: you provide a constant view of the collection (cannot add/remove elements, only offers a constant view of its elements) and as such it is safe (and a common idiom). No need to work out interfaces or other tricks to avoid outside code from changing your internals. – David Rodríguez - dribeas Sep 06 '09 at 11:29
  • @Matt: Agree, the nice part of const correctness is that it is 'transitive' in a sense. You provide a constant reference to a container that itself takes care of only providing constant views of its elements, each of which only offers constant views of its inner elements... – David Rodríguez - dribeas Sep 06 '09 at 11:31
  • 2
    Do remember that this is only a cast away from removing the read-onli-ness - namely, `(Customer) myCustomer`. Still, probably the best solution, if you behave and don't cast like that :) – Roman Starkov Jan 12 '10 at 23:52
  • @David: Languages provide these kind of mechanisms as an aid to development, not as a means to avoid inexperienced/bad programmers to do things the wrong way. In C# you achieve this in a natural way just by using interfaces, while in C++ you need an 'extra' feature called 'const'. It always looked to me that C++ is the one using a 'trick', not the other way around. – Trap Apr 09 '10 at 09:51
  • @David: Also, by using a read-only interface you're explicitly declaring your intentions, it's a design decision. I can't remember how many times I screwed things up due to forgetting to add the 'const' keyword. – Trap Apr 09 '10 at 10:02
  • 9
    @Trap, I agree that the mechanism is an aid for development which does include detecting errors from developers, experienced or not. I disagree that having to write read-only interfaces is a better solution in several grounds. The first point is that whenever you are writting a class in C++ you are implicitly defining the constant interface: the subset of members declared `const`, without the extra cost of defining a separate interface and requiring runtime dispatch. That is, the language provides a simple way of implementing compile-time const-interfaces. – David Rodríguez - dribeas Apr 09 '10 at 10:24
  • 12
    It is important to note that the `const`-ness **is** part of the design and it declares the intention just as clearly as writting an external const-interface. Also, providing const-interfaces can be a complex task if it has to be tackled manually, and might require writting almost as many interfaces as classes are present in the compound type. That takes us to your last sentece: 'screw things up due to forgetting to add the const'. It is easier to get used to add the `const` everywhere (development cost is small) than to write read-only interfaces for every class. – David Rodríguez - dribeas Apr 09 '10 at 10:29
  • @David: I'm not against the C++ 'const' keyword :) I just like the way const-correctness is achieved in C# more. I think that the amount of words you have to type to get things done can never be an important factor. As a note, in every modern language/IDE that I know you can extract a read-only interface with a few mouse clicks. – Trap Jul 21 '10 at 07:52
  • 2
    If you hold a big enough mutable object inside your own object, how will you provide a read-only interface to the containing object? Will you create a copy of the contained object? This is something you only miss when you learn how powerful having const-references is. – David Rodríguez - dribeas Jul 21 '10 at 08:00
  • As I mentioned in comment #3, and if size really matters, the container can provide a special read-only interface to handle that particular and insanely big object. – Trap Jul 21 '10 at 16:31
  • 9
    One of the reasons I really liked C# when it came out was that it didn't throw out all the pragmatic features of C++ like Java did. Java tried doing everything with interfaces, and it was a disaster. The proof is in the pudding. Over time a lot of features have been added "back" into Java that were originally denounced as heresy. Interface-based design has it's place, but when taken to extremes can unnecessarily complicate a program's architecture. You end up spending more time writing boilerplate code and less time getting useful stuff done. – kgriffs Nov 22 '11 at 20:38
  • 3
    I would favor "IReadableCustomer" as a name for the interface, rather than "IReadOnlyCustomer", because the latter would imply a contradiction when inherited by e.g. "IReadWriteCustomer" [If the leading character of interface names were lowercase, I'd use "iMutableCustomer", but "IMutableCustomer" looks too much like "ImmutableCustomer"] – supercat Jan 30 '12 at 23:47
  • 2
    @supercat Why not simply `ICustomer` for the name of the mutable interface? I believe this follows Microsoft's naming pattern, e.g., [ICollection](https://msdn.microsoft.com/en-us/library/92t2ye13(v=vs.110).aspx) and [IReadOnlyCollection](https://msdn.microsoft.com/en-us/library/hh881542(v=vs.110).aspx). – DavidRR Sep 19 '16 at 15:15
  • @DavidRR: An `IList` does not promise that implementations will be mutable, nor that they won't; it generally forfeits covariance for the ability to allow customers to mutate implementations that happen to be mutable without needing a cast (though `IList` supports a form of broken covariance with arrays: given references to `IList` and `Animal`, the only way of telling whether one can store the latter into an element of the former is to try it; if e.g. the `IList` is actually a `Cat` and the `Animal` is actually a `Dog`, storing it into the `IList` would fail. – supercat Sep 19 '16 at 15:50
28

To get the benefit of const-craziness (or pureness in functional programming terms), you will need to design your classes in a way so they are immutable, just like the String class of c# is.

This approach is way better than just marking an object as readonly, since with immutable classes you can pass data around easily in multi-tasking environments.

Sam
  • 28,421
  • 49
  • 167
  • 247
  • 8
    but immutability doesn't really scale to complex objects, or does it? – tenpn Sep 22 '08 at 10:58
  • 8
    I'd argue that if your object was so complex that immutability was impossible, you'd have a good candidate for refactoring. – Jim Burger Sep 23 '08 at 04:54
  • 2
    I think this one is the best of the group. Immutable objects are used too infrequently. –  Sep 25 '08 at 11:10
  • 3
    Not only that, but there are cases where you do want to have changing objects (objects do change!) but still offer a read only view in most cases. Immutable objects imply that whenever you need to make a change (change happens) you will need to make a new object with all the same data besides the change. Consider a school that has school rooms, students... do you want to create a new school each time a student's birthdate goes by and her age changes? Or can you just change the age at the student level, the student at the room level, maybe the room at the school level? – David Rodríguez - dribeas Sep 06 '09 at 11:41
  • 8
    @tenpn: Immutability actually scales incredibly well, when done right. One thing that really helps is the usage of `Builder` classes for large immutable types (Java and .NET define the `StringBuilder` class which is just one example). – Konrad Rudolph Mar 29 '10 at 10:51
  • 1
    @KonradRudolph, let's say you have 4GB of RAM, you have matrix 1Kx1K of floats. Please compute function `transpose` (with immutable matrix). The class is trivial, the function as well, and somehow you have the problem -- how come? – greenoldman Mar 05 '14 at 20:55
  • @greenoldman Things like this are actually a quite special case – pure languages handle them by special-casing such immutable functions (which, I might add, often works very well). But yes, that is one of the cases where you usually have to resort to mutability. Not because the object is *complex*, mind you – simply because of memory limitation. – Konrad Rudolph Mar 05 '14 at 22:06
  • @greenoldman Actually, sorry, let me backpedal. I’ve had a pint too many. There’s no need for special-casing on the language level. You can have an immutable interface for a `matrix` class and *still* design its API so that transposition can be done efficiently in place when self-assignment is done – in C++, the code `m = t(m);` can be made to work in-place even if `auto o = t(m);` would *not* operate in place and rather generate a new instance. If I had to design a matrix library, this is how I would design it. – Konrad Rudolph Mar 05 '14 at 22:13
  • @KonradRudolph, of course you can optimize assignment, no problem here, but immutable function `transpose` works as factory, thus **it** (not the caller) has to have another matrix, leading to 8GB of used space. And having only immutable objects (and functions) at hand, the caller, does not even have a choice to sacrifice pureness in order to gain in performance. My point is -- immutability is not a silver bullet or **replacement** for mutability. – greenoldman Mar 06 '14 at 06:45
  • @greenoldman To repeat, you can easily implement the operation `m = t(m);` so that *no* extra space is required – neither outside of `t` or inside it; it works *in-place*. Of course then `m` isn’t immutable, but the *interface of `t`* is that of an immutable function. And furthermore, this can be generalised, so that calls such as `matrix f() { matrix big_mat = /* some code */; return t(big_mat); }` can *also* work without requiring additional memory, even though the code is entirely immutable. – Konrad Rudolph Mar 06 '14 at 08:02
  • @KonradRudolph, "Of course then `m` isn’t immutable" -- sure, but we were talking about immutable objects. – greenoldman Mar 06 '14 at 17:59
  • @greenoldman I was never saying that immutability should be exclusive, you’re arguing a straw-man there. But like I said later, you *can* still make this purely immutable as well as in-place, see my example function. You just need to ensure that the previous object goes out of scope at the same time. This is how purely functional languages solve the conundrum. – Konrad Rudolph Mar 06 '14 at 18:26
24

I just wanted to note for you that many of the System.Collections.Generics containers have an AsReadOnly method which will give you back an immutable collection.

Rick Minerich
  • 3,078
  • 19
  • 29
4

C# doesn't have such feature. You can pass argument by value or by reference. Reference itself is immutable unless you specify ref modifier. But referenced data isn't immutable. So you need to be careful if you want to avoid side effects.

MSDN:

Passing Parameters

aku
  • 122,288
  • 32
  • 173
  • 203
  • well I guess that's what the question comes down to then: what's the best way to avoid the side effects of not having a const structure? – tenpn Sep 22 '08 at 10:42
  • Unfortunately only immutable types can help it. You can have a look at Spec# - there are some interesting compile time checking. – aku Sep 22 '08 at 13:28
2

Interfaces are the answer, and are actually more powerful than "const" in C++. const is a one-size-fits-all solution to the problem where "const" is defined as "doesn't set members or call something that sets members". That's a good shorthand for const-ness in many scenarios, but not all of them. For example, consider a function that calculates a value based on some members but also caches the results. In C++, that's considered non-const, although from the user's perspective it is essentially const.

Interfaces give you more flexibility in defining the specific subset of capabilities you want to provide from your class. Want const-ness? Just provide an interface with no mutating methods. Want to allow setting some things but not others? Provide an interface with just those methods.

munificent
  • 11,946
  • 2
  • 38
  • 55
  • 15
    Not 100% correct. C++ const methods are allowed to mutate members marked as `mutable`. – Constantin Sep 25 '08 at 11:21
  • 3
    Fair enough. And const-casting lets you get rid of const-ness. Both kind of imply that even the C++ designers realized one-size-fits-all is really one-size-fits-most. – munificent Sep 25 '08 at 15:13
  • 8
    The advantage of C++ is that you have const for most cases, and you can implement interfaces for others. Now, besides const, C++ has the mutable keyword to apply to attributes as caches of data, or locking mechanisms (mutexes or the like). Const is not 'will not change any internal attribute) but rather will not change the object state as perceived from outside. That is, any uses of the object before and after calling a const method will yield the same result. – David Rodríguez - dribeas Sep 06 '09 at 11:34
  • 9
    casting away const to mutate comething in C++ is undefiend behaviour (nasal demons). const_cast is for interfacing with legacy code that while being logically const isn't marked const. – jk. Mar 29 '10 at 10:51
2

Agree with some of the others look at using readonly fields that you initialize in the constructor, to create immutable objects.

    public class Customer
    {
    private readonly string m_name;
    private readonly int m_age;

    public Customer(string name, int age)
    {
        m_name = name;
        m_age = age;
    }

    public string Name
    {
        get { return m_name; }
    }

    public int Age
    {
        get { return m_age; }
    }
  }

Alternatively you could also add access scope on the properties, i.e. public get and protected set?

    public class Customer
    {
    private string m_name;
    private int m_age;

    protected Customer() 
    {}

    public Customer(string name, int age)
    {
        m_name = name;
        m_age = age;
    }

    public string Name
    {
        get { return m_name; }
        protected set { m_name = value; }
    }

    public int Age
    {
        get { return m_age; }
        protected set { m_age = value; }
    }
  }
  • 3
    These are different approaches that do not really fit the whole problem. With access control level you only allow deriving classes from changes, in many cases this will not model the real world appropriatedly. A teacher does not derive from a student record but may want to change the student grades, although the student cannot change the grades but can read them... just to name a simple example. – David Rodríguez - dribeas Sep 06 '09 at 11:44
1
  • The const keyword can be used for compile time constants such as primitive types and strings
  • The readonly keyword can be used for run-time constants such as reference types

The problem with readonly is that it only allows the reference (pointer) to be constant. The thing referenced (pointed to) can still be modified. This is the tricky part but there is no way around it. To implement constant objects means making them not expose any mutable methods or properties but this is awkward.

See also Effective C#: 50 Specific Ways to Improve Your C# (Item 2 - Prefer readonly to const.)

Thomas Bratt
  • 48,038
  • 36
  • 121
  • 139