7

Context

I recently read about the object-oriented technique of making a distinction between objects and passive data structures, best summarized in Clean Code:

"Objects hide their data [...] and expose functions [...]. Data structures expose their data and have no meaningful functions."

I am considering using C# structs for passive data structures.
To clarify this: IF part of my code needs to function as a passive data structure, THEN I want to use a struct for that.

Advantages

It would provide a language-given distinction between objects and passive data structures.

Also, if an object has a private field of a class, but exposes that in a function, it can be changed somewhere else. That means the object data is changed from the outside, which is not good. I know you should e.g. expose an internal List as ReadOnlyList, but that is good practice, which even good programmers don't always follow. Using structs instead would automatically enforce this.

What I found out so far

I know the question "When to use struct" is already answered several times. The answers always boil down to the advice from the official docs:

AVOID defining a struct unless the type has all of the following characteristics:

  • It logically represents a single value, similar to primitive types (int, double, etc.).
  • It has an instance size under 16 bytes.
  • It is immutable.
  • It will not have to be boxed frequently.

I think the first 2 points are for improving performance on the stack. However, as far as I understand structs are better on the stack, but not worse on the heap.

Point 3 I can still adhere to. Might make the code cleaner, might make it more awkward, but I don't know yet.

Point 4 is also about performance improvements, but I also actually don't need a lot of performance. Even if, at this point that would be early, early optimization - I'm not working with big data here.

With a name like that, I want to think structs are exactly the thing to use for object oriented passive data structures. The documentation from the official docs makes me doubt that though, especially the size limitation. Even 2 strings for an address with 2 rows would already be too much.

The question

Are there other arguments against using structs for these passive data structures? Or did I understand something wrong?

Example

public struct EmployeeId // data structure (exposed data, no functions)
{
    public string Value;
}

public struct Address // data structure
{
    public string Line1;
    public string Line2;
}

public struct Performance // data structure
{
    public int Successes;
    public int Failures;
}

public struct Employee : IEquatable<Employee>
// data structure
{
    public EmployeeId Id:
    public Address Address;
    public Performance Performance;

    public bool Equals(Employee employee)
    {
        return Id == employee.Id;
    }
}

public class OfficialEmployeeRegistry // object (hidden data, exposed functions)
{
    private Dictionary<EmployeeId, Employee> _employees;

    public void Add(Employee employee)
    {
        _employees.Add(employee.Id, employee);
    }

    public List<Employee> GetPositivePerformers() {...}
}

public class SantaClause // object
{
    private EmployeeRegistry _employeeRegistry;
    private PresentSender _presentSender;

    public void SendChristmasPresents()
    {
        List<Employee> goodEmployees = employeeRegistry.GetPositivePerformers();
        foreach(Employee employee in goodEmployees)
        {
            _presentSender.SendPresent(employee.Address);
        }
    }
}

All structs in this code are examples of what I want to do. For example, we can get the performance of an employee from the OfficialEmployeeRegistry now. We can send that data to a printer class, but if that class changes it in the process, the entries in the OfficialEmployeeRegistry are protected. OfficialEmployeeRegistry data will only be manipulated by itself. Oh, and the structs are supposed to be immutable of course, but I feel adding a constructor to each would bloat this post to much.

Reaction to commments

Do you require data serialization?
No.

Will this need to be passed into and from functions/methods?
Yes.

Will it be iterated and modified on a fairly significant basis?
No. I guess this is about performance; but performance is definitely not an issue

  • 1
    Downvoters care to explain? I'd like to think the question shows research effort and is clear and useful. – R. Schmitz Dec 22 '17 at 11:17
  • Could you give an example of a class that you are thinking of turning into a class? – jmoreno Dec 22 '17 at 13:42
  • Just a few thoughts off the top of my head: Do you require data serialization? Will this need to be passed into and from functions/methods? Will it be iterated and modified on a fairly significant basis? Also: immutability on a monolithic basis means that you need to "batten down the hatches" on multiple levels of concerns (not a bad thing, just a... thing). – code4life Dec 22 '17 at 13:47
  • 3
    NOTE: I will warn you that working with structs in lists will not behave the way you expect them to. Mixing and matching value types with object types will lead to a number of unexpected maintenance headaches. Try it for a while, then ask a colleague to implement a change to your code. The feedback will be very informative. – Berin Loritsch Dec 22 '17 at 17:57
2

What speaks against using structs for passive data structures?

Technically, nothing.

(if you can live with the some restrictions mentioned by Erik Eidt, like no inheritance).

I am pretty sure if you try to use C# structs for all of your data objects, you can still write correct programs following this convention. The 16 byte size limitation is only a recommendation for performance, and can be safely ignored.

However, the real question you should ask yourself is, does this convention produce better maintainable, evolvable, readable programs, compared to programs which follow the more usual C# convention to use classes in most cases, even for most kind of DTOs or "passive data structures", and structs only for exceptional cases.

And that is IMHO questionable. I usually prefer to use classes first and foremost in my programs (even for pure data objects, when I only need a list of public member functions), and use struct only for such optimizations like mentioned by @Ivan. The reason is I made the experience that often when I start with a small data structure where a simple struct would be suffient, over time new requirements to this structures cause some evolvement where then a full class starts to make more sense. And if I have used that struct in various places of the program, changing it into a class afterwards can introduce a lot of subtile errors because of the switch from value semantics to reference semantics.

Moreover, if a type is defined as a struct or as a class is not immediately detectable when reading code using that type. However, there are subtile semantic differences between the two (see the comment from @BerinLoritsch below the question). Using struct only for exceptional optimization cases makes errors in my code caused by those differences less likely.

So my recommendation here is, use structs exclusively for passive data structures when you know for sure this particular data structure will never ever evolve to a class, and only if you have a useful optimization scenario. And if in doubt, better use a class.

  • Your analysis of the situation is spot on. I changed "data structure" to "passive data structure". I don't really like DTO as name for these, as it might be used internally by an object without any transfer ever happening. Now, changing later on would indeed be painful, but I can't imagine how that would happen in the first place. How does data turn into something you want to do? I'd suspect you want another object working on that data instead. – R. Schmitz Dec 23 '17 at 15:40
  • @RaphaelSchmitz: If it doesn't contain any methods, then such a class is a data transfer object by definition, since the only thing it is capable of is transferring a bundle of data from one place to another. – Robert Harvey Dec 24 '17 at 1:52
  • @Robert Harvey After my blunder with the name confusion, I'm going to rely on Wikipedia for my term definitions from now. What they have to say about this matter is "A value object is not a DTO. The two terms have been conflated by Java developers in the past." Are you by any chance a Java developer from the past? ;) And then the answerer is "Doc Brown" - It's getting very time travely here... – R. Schmitz Dec 24 '17 at 11:13
  • 1
    I am accepting this as the answer. The reason is that what I'm suggesting hasn't been tried yet, as far as the answers go. We don't know if this will work or fail yet and this answer doesn't try to guess that. Instead, it mentions actual practical difficulties in a situation encountered in current everyday programming, which would be applicable to my suggested way of doing things. – R. Schmitz Dec 29 '17 at 21:57
10

You're confusing the struct keyword with the term "data structure." They are two very different things. Greatly simplifying, a struct in C# is basically a class with value semantics, whereas a data structure is a collection of data with storage and retrieval algorithms that have specific performance characteristics.

"Objects hide their data [...] and expose functions [...]. Data structures expose their data and have no meaningful functions."

Given that definition, what you're looking for is not a struct, but rather a class with data members only (no functions). structs have value semantics, which is probably not what you want to put in a collection unless you really are collecting values.

Accordingly, using the struct keyword to signal that you're collecting data with no behavior is probably not a good approach. Competent software developers already know that their data structures are going to contain classes of objects; your use of a language keyword that suggests otherwise will be confusing to them.

Use struct to indicate value semantics, not to indicate "data-only objects."

See Also
Data Transfer Object
Data Structure
Passive Data Structure

  • I'm sorry, but I feel like you must have just skimmed my question? – R. Schmitz Dec 22 '17 at 0:55
  • 2
    Your first paragraph starts with "You're confusing A and B" My question literally asks about "using A for B". I also carefully marked the language features "class" and "struct" as code and the OO concepts "object" and "data structure" in bold. I also completely concur with your take on what those things mean. I even reread my question a couple of times, but I can't find any part where I talk about the language features as if they were OO concepts or vice versa. – R. Schmitz Dec 22 '17 at 8:40
  • 2
    Then the second part of your answer; my question is "Are there arguments against using a [struct instead of class]?" Your answer is "you're looking for a class". That is not an answer to a yes-or-no question and in some way the opposite of what I'm asking for. You probably see how answering a different question makes it seem as if you only picked up a few keywords from the question instead of reading it. However, maybe I'm just really bad at explaining. Maybe look at Erik Eidt's answer, he got it right. – R. Schmitz Dec 22 '17 at 9:18
  • 3
    At this point, with three upvoted answers that you think are not addressing the question you have, you should strongly consider revising your post, @RaphaelSchmitz. If everybody's misunderstanding you, it's likely that you're not saying what you think you're saying. – Josh Caswell Dec 22 '17 at 15:44
  • 2
    Well, you still wouldn't use a struct. You seem to be getting mired in vocabulary, but missing the entire point of your question, which is this: "Should I use struct to indicate a passive data structure?" The answer is "No, because doing so also changes the semantics of your object. Use struct only when you want the semantics that the struct keyword provides." – Robert Harvey Dec 23 '17 at 16:50
5

The C# struct concept, as you are seeing elsewhere, is a value notion.

Thus, a field of type struct is a value, not a reference. A field of type struct is an embedded value.

This doesn't map particularly well to recursive data types, like trees and lists. These recursive data types need multiple entities that can reference each other.

In order to get multiple objects and references, you need to box the structs, so they become objects. This can be done a number of ways. One is to use interfaces, but now you're implementing methods (though mostly just getters). Another is to use unsafe pointers. And yet another is to upcast, which loses valuable (compile time) type information. None of these is attractive compared to the simplicity of classes with public fields.

C# structs also don't play nicely with inheritance and polymorphism (or tagged unions). We can't use one struct as the base for another. Though we can embed one in another, that is also awkward when we wanted polymorphism, which we might want to use in some recursive data types.

Classes can be made immutable, support polymorphism, and references, so work rather well for data structures, especially recursive data types.

  • I haen't thought about that yet, but there's also a couple of things I don't understand. A) "recursive data types need multiple entities that can reference each other" - the items in a list don't know about each other though? Except in a linked list? B) "you need to box the structs [...] This can be done a number of ways" - Doesn't List<MyStruct> work? C) "C# structs also don't play nicely with inheritance and polymorphism" - It will enforce favoring composition over inheritance. I also feel like the data shouldn't really inherit. Polymorphism is still possible with interfaces. – R. Schmitz Dec 22 '17 at 1:17
  • "the items in a list don't know about each other though?" It depends on the level of abstraction that you're looking at. From the client's perspective, using List or some IList implementation, sure the elements added to the list don't know about each other. From a List implementer's perspective, even though the elements don't know about each other, there are other (internal) objects and references that comprise core of the list data structure that do know about each other. These provide for core capabilities (e.g. of lists, trees) such as insert, delete, iterate, find, etc.. – Erik Eidt Dec 23 '17 at 1:16
  • But I AM a client. AFAIK the consensus on how to do your own IList etc. implementations is "Don't", as it probably already has been done, better than your homebrew will turn out. I seriously expect to never write an IList implementation my whole life. – R. Schmitz Dec 23 '17 at 13:35
2

It's amazing how just changing the spin of a statement will change its sentiment...

Choose a struct instead of a class if:

  • It logically represents a single value, similar to primitive types (int, double, etc.).
  • It has an instance size under 16 bytes.
  • It is immutable.
  • It will not have to be boxed frequently.

Some other points to consider:

  • All the fields of a struct should themselves be structs or primitive types.
  • If identity doesn't make much sense for the data type, then make it a struct. (I.E. if two equal objects are essentially indistinguishable.)

To be specific and direct:

Are there other arguments against using structs for these data structures? Or did I understand something wrong?

There is no argument against using struct for data structures that satisfy the conditions above.

Regarding types that don't quite satisfy them...

If your type has to be boxed frequently, then the type probably doesn't represent a value.

If your type isn't immutable, then it doesn't behave like a value.

If the instance size is over 16 bytes but otherwise fulfills the requirements of being a struct, then have the type hold a heap object internally so it will be 16 bytes.

If it isn't logically a single value, then it should not be a struct.

  • That is the answer to that other question, "When to use struct". – R. Schmitz Dec 22 '17 at 0:56
  • 1
    That's the point. If the data type satisfies the criteria, there is no reason to avoid using a struct. – Daniel T. Dec 22 '17 at 3:35
  • They don't though. That's the point. Of this question. An answer to a when question also can't be an answer to a yes-or-no question. Although I have to admit, it's about pretty abstract stuff. You're not the only one with an answer for a different question. I am pretty certain though that it can not be answered by inverting some bool conditions in the docs. – R. Schmitz Dec 22 '17 at 11:05
  • Hopefully my addition will be more to your liking. – Daniel T. Dec 22 '17 at 13:34
  • @DanielT., the 16 byte caveat is mainly for performance since the value is passed by copy. You could argue that if it isn't done often it could still be a struct. There is one reason to use a struct that is outside of these parameters, and that's for IProgress<T> which is intended to let you use a struct to keep track of where you are and send a snapshot to the handler to update a progress bar or something like that. – Berin Loritsch Dec 22 '17 at 17:50
0

The biggest distinction in behavior between a struct and a class is that a struct is passed by value while a class is passed by reference. There are many surprises you can run into that look correct but don't behave correctly--because the code was written under the assumption that it was working with a class. Surprises are bad for people who have to maintain the software.

What that means is the following code can behave very differently depending on whether Message is a struct or a class:

public static void CreateTitle(Message message)
{
    message.Title = "Created the Title";
}

public static void Main()
{
    Message m = new Message
    {
        Title = "Unset",
        Content = "Message content"
    };

    CreateTitle(m);

    Console.WriteLine($"> {m.Title}: {m.Message}");
}

If Message is a class you will see:

> Created the Title: Message content

If Message is a struct you will see:

> Unset: Message content

This is actually desirable when you are using a struct to pass progress state through an event. Essentially your processing code can keep counting and a snapshot of the struct is copied to the event handler.

However, if you are dealing with general data management, you will most likely need the semantics of a class. The reasons for the list of caveats from Microsoft is to force you to think about the implications. In most cases you'll want the behavior of a class.


Bonus fact, you can make the CreateTitle() method behave the same regardless of whether Message is a struct or a class if you explicitly tell it to pass by reference:

public static void CreateTitle(ref Message message) { /* .... */ }

Disclaimer: This is a contrived example to show the practical implications of the types of surprises to expect when using struct instead of class. I don't actually write code this way.

  • You say "if you are dealing with general data management, you will most likely need the semantics of a class." but even according to your own explanation the behaviour I am asking for is that of value types. – R. Schmitz Dec 23 '17 at 0:06
  • If that's the behavior you want, then go for it. Just keep in mind that you want to avoid mixing objects and structs as much as possible or weird things can happen that are hard to explain (personal experience). I also wouldn't worry about the 16 byte rule until you actually have a measured performance problem. – Berin Loritsch Dec 24 '17 at 1:19
0

Conceptually, the difference between a class and a struct is that a struct is a more basic, non-object-oriented feature.

Internally, classes are implemented as a programming pattern built upon structs - the extra behaviour and functionality which classes have can be entirely re-implemented in terms of structs and procedural code.

The distinction is retained (i.e. structs remain in modern languages) mainly for performance reasons on the one hand (classes consume more memory and have overheads in their execution, and are stored by default on the heap instead of the stack), and interop reasons on the other (i.e. to interact with external code that is written and called in a non-object-oriented way - either due to it's age or due to performance reasons in turn).

When I say performance, to be clear in most cases I'm talking about an extra couple of bytes of memory, or a few extra machine instructions per method call - a grave inefficiency 40 years ago, but not often nowadays (except for certain operating system functions or specific algorithms). The same arguments were had over the use of GOTO statements (i.e. the argument between structured and unstructured language features).

The underlying difference is also blurred in many general purpose languages.

For example, some object-oriented features, like member methods, can be added to structs without any additional overhead - but those members are not implemented as function pointers stored within the struct, as they are in classes.

It is the storage, and re-writing, of internally-stored function pointers, which permits the member methods of classes to be: (a) overridden without rewriting (or re-implementing) the code of the base class, and (b) called directly from external code, in a way that member methods on structs cannot be.

In most cases nowadays, any use of structs where a class will do, is a premature and unnecessary optimisation.

-1

Classes aren't just about retrieval. They can also be used to enforce contracts and invariants on your data: for example, that their address is valid. If you aren't checking for invariants on your data then go ahead, use a struct. Otherwise, consider using a class instead.

  • 1
    In OO, you should NOT do that. Address.Validate() is "using OO features", but not "writing OO code". In Germany, an address is a street, a house number, a zip code and a city name. The UK have it similar, but instead of a house number you can have something like "The blue house at the end of the street". In the Netherlands, a zip code and a house number is enough for mail to arrive at the correct place. So now Address has 3 different ValidateCountryX() and one Validate() which decides which one too use. Those functions are related to each other, so you probably want to group them together. – R. Schmitz Dec 23 '17 at 13:19
  • What is the way to group functionality in C#? Classes. class AddressValidator. Now these things behave like you'd expect them to: An address - like"Baguette street 3, Paris" - doesn't know if it's valid. It doesn't have a GoogleMapsApi and makes an asynchronous call to check whether it is correct. An AddressValidator doing and having those things does make a lot of sense though. Also, in 100 years: class MarsAddressValidator - BAM functionality added without even needing to touch any existing code. – R. Schmitz Dec 23 '17 at 13:19
  • @RaphaelSchmitz that's a limited view of OO. Carefully verifying class invariants is central to Bertrand Meyer-style Design by Contract OOP, which is highly effective for writing high-assurance and formally verified object code. Granted, Eiffel and Ada are a little more suited to DbC than C#, but it's certainly untrue that "you should NOT do that". – Hovercouch Dec 23 '17 at 16:37
  • Design by contract vs defensive design is orthogonal to OO vs functional. I am not an authority, so I can't really tell anybody what they should and shouldn't do, so let me correct that: "Address.Validate() is not OO code". I mean, if you ask a child "who can I ask if this address is right" and it answers "the address itself", you should get that child to a shrink. – R. Schmitz Dec 23 '17 at 17:13
  • On the other hand, it's both reasonable and widely accepted to say that the Address carries the type information of being an Address. It's also good practice to place database constraints on your Address table as opposed to making a separate "Address Validation" table. Preserving invariants isn't a crazy thing at all. – Hovercouch Dec 23 '17 at 17:26

protected by gnat Dec 30 '17 at 6:16

Thank you for your interest in this question. Because it has attracted low-quality or spam answers that had to be removed, posting an answer now requires 10 reputation on this site (the association bonus does not count).

Would you like to answer one of these unanswered questions instead?

Not the answer you're looking for? Browse other questions tagged or ask your own question.