1

TL:DR; is it bad to make a struct where the value does something to initialize itself at the beginning of get for the struct itself (no public properties, but any comparison/etc. executes initialization), and if so why?


I'm wondering how bad an idea it is to generate what is effectively the default value for a struct in an effectively-immutable way. I've read about how bad it is to have a mutable struct, etc, but what if you have a struct with no public properties - it in itself is the value it represents - and that value corresponds to some external immutable resource?

For example, consider the following struct:

using System;
public struct Computer
{
    private string _name;
    private string _domain;
    private bool _isInitialized;

    public static Computer Parse(string name)
    {
        if (string.IsNullOrEmpty(name))
        {
            throw new ArgumentNullException("name");
        }
        var result = new Computer();
        result._name = string.Copy(name);
        result._domain = string.Empty;
        result._isInitialized = true;
        return result;
    }
    public static Computer Parse(string name, string domain)
    {
        if (string.IsNullOrEmpty(name))
        {
            throw new ArgumentNullException("name");
        }
        var result = new Computer();
        result._name = string.Copy(name);
        if (_domain == null) { result._domain = string.Empty; }
        else { result._domain = string.Copy(domain); }
        result._isInitialized = true;
        return result;
    }
    private void Initialize()
    {
        if (!_isInitialized)
        {
            var source = System.Net.NetworkInformation.IPGlobalProperties.GetIPGlobalProperties();
            _name = source.HostName;
            _domain = source.DomainName;
            _isInitialized = true;
        }
    }
    public override string ToString()
    {
        Initialize();
        if (!string.IsNullOrEmpty(_domain)) {
            return _name + "." + _domain;
        }
        else {
            return _name;
        }
    }
    public override bool Equals(object other)
    {
        Initialize();
        if (other is Computer)
        {
            var otherComputer = (Computer)other;
            return _name.Equals(otherComputer._name, StringComparison.OrdinalIgnoreCase) &&
                _domain.Equals(otherComputer._domain, StringComparison.OrdinalIgnoreCase);
        }
        else 
        {
            return false;
        }
    }
    // additional comparison methods omitted.
}

As seen, any operation for comparison, etc. of the Computer entity will result in initialization - effectively, observing the Computer will result in it no longer being a 0-byte value.

Why would I do such a thing? I want something akin to an immutable value type that is, by default, representative of an actual value - as opposed to a truly default struct that would be 0 bytes. I can then use such a thing as a default value to a parameter: public void DoSomething(Computer computer = default) and know that default means the local device - if this were not a ValueType, I would have to pass in null as the default for the parameter as it's impossible to have a constant reference type (i.e. can't have public void DoSomething(string computerName = Environment.MachineName)).

What this means is that the true "default" is never really observed - and the struct can never be read as a 0 byte value, which I have read is what a value type's default essentially is.

Why shouldn't I do this - or is it totally OK? Are there instances in well-known code where this practice is applied?

Stroniax
  • 718
  • 5
  • 15
  • I think what you are doing is just [the singleton pattern](https://en.m.wikipedia.org/wiki/Singleton_pattern). It is quite common. In the latter years it is not that popular anymore tough. – JHBonarius Mar 13 '21 at 07:39
  • Might I ask why you are using a struct in the first place? the `_value` is a string = System.String is a reference type. There are more weird quirks in your code, like a "default null" parameter for your initializer (which is probably a hack?). – JHBonarius Mar 13 '21 at 08:15
  • @jhbonarius perhaps ComputerName would have been a better example. I could write a hundred methods that operate on “a computer” and require a `ComputerName` parameter, which I want to not be just a string. And by providing `default` (instead of “.”, “localhost”, etc.) one may indicate the method should target the local device - BUT you could also use `ComputerName.Parse(string)` to reference a different computer. This adds clarification and simplification about default value behavior. The Local singleton may be misleading; that’s just a cached default, much like `CancellationToken.None`. – Stroniax Mar 13 '21 at 08:19
  • 1
    Ok, why not a simple class that has only a readonly string parameter? Or [a record](https://learn.microsoft.com/en-us/dotnet/csharp/tutorials/exploration/records): `record Computer(string Name);` <-- that's the whole definition and implementation. You can then just `var myComputer = new Computer("MyName");`. Why would you want a singleton (single static instance)? – JHBonarius Mar 13 '21 at 08:27
  • In response to the latter question, many structs include reference type members. The struct represents a service tag and is therefore its own distinct entity, but the data is stored as a string. If this is poor practice I would like to understand why - there’s still a lot I don’t understand. Null CimSession will use a local session - operating on the executing computer. I think I gave that parameter a default just for the sake of faster testing. – Stroniax Mar 13 '21 at 08:28
  • The singleton cached the local instance so that I don’t have to create the CimSession and query from CIM every time I get a `default(ServiceTag)`, for efficiency’s sake. I don’t want a null value, and I want to be able to provide, for example, the local device as a `const` - so the default should clearly reference the local machine, hence using a struct. I have not heard of `record` before and will look into it - thank you. – Stroniax Mar 13 '21 at 08:35
  • a `record` is just a non-mutable `class` (under the hood a class is generated). And the arguments result in properties being generated. Why use a struct over a class? "represents a service tag and is therefore its own distinct entity" <- that is something you understand, but "service tag" is not a universal general concept (I fond something about Dell PCs on google, but what then is a "CIM"?), please elaborate. Warning: watch out with temporary "test code": in my professional experience I've encountered bugs in production due to 'test code' still being there. It's also confusing here. – JHBonarius Mar 13 '21 at 09:00
  • (note, I'm just asking to better understand your issue, so I can give a better answer). `default(ServiceTag)` would indicate that you create a new object... but that would refer to a static object? Will your business logic have multiple `ServiceTags`? It's not fully clear to me what the actual setup is. – JHBonarius Mar 13 '21 at 09:18
  • `record` does not imply immutability. I can very easily have a mutable record. The short-form just happens to declare immutable properties – pinkfloydx33 Mar 13 '21 at 09:45
  • @JHBonarius CIM is very similar to WMI if you're familiar with that - this is basically the modern `System.Management`, to access data about the device. I'm considering this application for `struct` in a fleet of Dell devices - the dell Service Tag is effectively the immutable serial number of the motherboard. Being a ValueType, you're not actually getting the singleton with `default`/`new` - you'll *always* get a new item - it'll just have an equal value to the singleton. – Stroniax Mar 13 '21 at 17:06
  • Perhaps I can clarify further. What I want - the reason I'm looking into doing this - is I'd like a value for which the default is not `null`, but is representative of a value on the executing host. In this manner, I will be able to access the constant value representing the local device through `default`, which provides more clarity than `null` IMO. – Stroniax Mar 13 '21 at 17:17
  • @JHBonarius I have rewritten the example in what I hope to be more clear - let me know if there's any further clarification I can provide. – Stroniax Mar 13 '21 at 17:48
  • @Stroniax [`String.Copy`](https://learn.microsoft.com/en-us/dotnet/api/system.string.copy?view=net-5.0) is obsolete, anyway thanks! After 10 years of C#, I did not even known this (weird and useless) method existed! – Orace Mar 13 '21 at 18:03
  • By "getter", what exactly are you talking about here? Usually, the term "getter" is used in conjunction with properties, of which there are none in your code example. – Lasse V. Karlsen Mar 13 '21 at 21:10
  • @LasseV.Karlsen When I used the term I referred to any public operation that observes the value. – Stroniax Mar 13 '21 at 22:21

1 Answers1

2

Is is generally a bad idea to do anything but return a value in a getter.

  • The user does not expect the getter to throw.
  • The user does not expect the getter to take a lot of time.
  • The user does not expect the getter to modify the internal state of the object.

About the struct being read-only, in newer version of C# you can declare a struct to be read-only, that allow some optimization, and it's easier to maintain (the fact that the struct is read-only is obvious).

Here is an implementation of a readonly struct Computer where default(Computer) acts like Computer.Local:

public readonly struct Computer
{
    // Use a singleton for the local computer.
    public static Computer Local { get; }

    static Computer()
    {
        var source = System.Net.NetworkInformation.IPGlobalProperties.GetIPGlobalProperties();
        Local = new Computer(source.HostName, source.DomainName);
    }

    private readonly string _name;
    private readonly string _domain;
    private readonly bool _isNotDefault;

    private Computer(string name)
    {
        _name = name ?? throw new ArgumentNullException(nameof(name));
        _domain = string.Empty;
        _isNotDefault = true;
    }

    private Computer(string name, string domain)
    {
        _name = name ?? throw new ArgumentNullException(nameof(name));
        _domain = domain ?? throw new ArgumentNullException(nameof(domain));
        _isNotDefault = true;
    }

    public static Computer Parse(string name) => new Computer(name);
    public static Computer Parse(string name, string domain) => new Computer(name, domain);

    public string Name => _isNotDefault ? _name : Local.Name;
    public string Domain => _isNotDefault ? _domain : Local.Domain;

    public override string ToString()
    {
        return string.IsNullOrEmpty(Domain) ? Name : Name + "." + Domain;
    }

    public override bool Equals(object other)
    {
        return other switch
        {
            Computer otherComputer => Name.Equals(otherComputer.Name, StringComparison.OrdinalIgnoreCase) &&
                                      Domain.Equals(otherComputer.Domain, StringComparison.OrdinalIgnoreCase),
            _ => false
        };
    }
}
Orace
  • 7,822
  • 30
  • 45
  • In this case I cannot provide `Computer.Local` as a default parameter value for a method, however - can I? A goal is that the `default(Computer)` will be a value indicating the local device so that I can define a method `DoSomethingToComputer(Computer computer = default)`, and therefore default = the local computer. Your point about getters not waiting/throwing make sense. I suppose I have based this model off a getter lazily constructing a field - such as `public string MyString {get => _string ??= "initial"; } private string _string;` – Stroniax Mar 13 '21 at 18:45
  • 1
    @Stroniax Read carefully, in my code `default(Computer).Name` returns the name of the local computer. – Orace Mar 13 '21 at 19:12
  • No, IMHO you are generalizing too much. For instance, you are talking about "the user", while there isn't already some user (except yourself). And define "a lot of time". Everything depends on the situation. – JHBonarius Mar 13 '21 at 19:27
  • @JHBonarius, see https://stackoverflow.com/a/1294189/361177 for some guidelines about getter (where `a lot of time` is defined as a db/network call). Indeed all of this is opinion based https://stackoverflow.com/questions/2784934/should-properties-in-c-sharp-perform-a-lot-of-work/2784974#2784974 and the language allow you to throw, take your time, modify the internal state in a getter. It's open to you to do it, but if you work in team or expose an API (ie: if you have *users* of your code), it's better to have some convention and respect them. – Orace Mar 13 '21 at 21:02