2

I'd like to ask something about generics.

I am trying to keep the code simple, and thus I will be making a single class to handle load/save for a game's savegame files. As each portion of the game has different requirements I'd like to keep this as easily accessible as possible:

public void Load<T>(string path, out T obj)
{
    BinaryFormatter bf = new BinaryFormatter();
    using (FileStream file = File.Open(Application.persistentDataPath + path, FileMode.Open))
    {
        obj = (T)bf.Deserialize(file);
    }
}

Now I can call this with a simple

TurnData x; s.Load("test.txt", out x);

The alternative would be to make the Load function return the object and then convert it to a TurnData type.

TurnData x = (TurnData)s.Load("test.txt");

I do not know much about C#. I assume that the code inside using(...) { ... } does not get executed if there is an error opening the file for example? If someone can confirm this that would be nice. The example code I have seen did not have any error handling, which seemed weird to me, so I added using?

So in this secondary version where the function returns the object instead of using an out parameter would need more complicated code for error checking and possible return null? It doesn't seem great.

So the real question is ... can I use the next version I have here or are there concerns that I should have due to the use of generics?

Nisarg Shah
  • 14,151
  • 6
  • 34
  • 55
Sir Rogers
  • 345
  • 1
  • 3
  • 10
  • 1
    `using` block is just simplified version of `try{} finally{ //will dispose disposable object }` – SᴇM Sep 23 '17 at 13:52
  • basically it's calls `Dispose()` method of your object, even if the code inside the block throws an exception. – SᴇM Sep 23 '17 at 13:55
  • I don't understand why you're writing the code using generics *at all*. Why is `Foo foo; Load(path, out foo);` in any way better than `Foo foo = (Foo)Load(path);` ? The latter seems much more clear to me, and shorter and simpler. Your suggestion of doing so has great merit and you should consider doing it. You're going to have to write error handling either way. – Eric Lippert Sep 23 '17 at 15:20
  • @EricLippert, isn't it rather the problem with the `out` variable? I don't really see why giving an output out would make it more readable? – Icepickle Sep 23 '17 at 19:47

2 Answers2

5

There is no generic code bloat for reference types - code is reused. With value types, though, CLR will generate a separate method for each type. See .NET Generics and Code Bloat.

Pavel Tupitsyn
  • 8,393
  • 3
  • 22
  • 44
  • I know I´ve read that, which is why I stated explicitly that Mono is being, where this does not necessarily apply – Sir Rogers Sep 23 '17 at 14:16
3

The using statement has nothing to do with error handling. Using File.Open method you can expect to get the exceptions you will find here. You could avoid the abruptly stop of your program from any such an exception by wrapping your using statement in a try/cath construct like below:

public T Load<T>(string path)
{
    T obj = default(T);
    var bf = new BinaryFormatter();
    try
    {
        using (var file = File.Open(Application.persistentDataPath + path, FileMode.Open))
        {
            obj = (T)bf.Deserialize(file);
        }
    }
    catch(Exception exception)
    {
        // Log the exception
    }
    return obj;

}

Essentially you attempt to Open the file specified in the path. If that fails you just log the failure and your return null from the function.

Regarding the using statement, it provides

a convenient syntax that ensures the correct use of IDisposable objects.

as you can read more thoroughly here

As a side note regarding the signature of your method I would make a few comments. Consider the following method body and spot the differences with that we have above.

public T Load<T>(string path, IFormatter formatter)
{
    if(path ==null) throw new ArgumentNullException(nameof(path));
    if(formatter == null) throw new ArgumentNullException(nameof(formatter));

    T obj = default(T);
    try
    {
        using (var file = File.Open(path, FileMode.Open))
        {
            obj = (T)formatter.Deserialize(file);
        }
    }
    catch(Exception exception)
    {
        // Log the exception
    }
    return obj;   
}

and

var path = Path.Combine(Application.persistentDataPath, "test.txt");
var binaryFormatter = new BinaryFormatter();
var x = s.Load(path, binaryFormatter);

Making the above changes you make your method more easily to be tested through a unit test and more reliable since you make some precondition checking before the meat and potatoes of your method. What would had happened if you had passed a null path? What would had happened if you had passed a null formatter ? etc...

Christos
  • 53,228
  • 8
  • 76
  • 108
  • 1
    This code does not compile: CS0403 Cannot convert null to type parameter 'T' because it could be a non-nullable value type. Consider using 'default(T)' instead. – Pavel Tupitsyn Sep 23 '17 at 14:05
  • 1
    @PavelTupitsyn You are pretty damn correct ! I just corrected it. Thanks – Christos Sep 23 '17 at 14:06
  • @StuartLC Thank you very much for your comment. Pavel also pointed this out :) – Christos Sep 23 '17 at 14:06
  • 2
    The example code above violates several C# best practices: don't throw NullReferenceExceptions. There is an exception type specifically for this situation: ArgumentNullException see: https://stackoverflow.com/questions/22453650/ . Also, don't catch general Exceptions see: https://stackoverflow.com/questions/1742940/ – Dave M Sep 23 '17 at 23:44
  • @DaveM Thanks for you comment. WIth the first point I totally agree with you. My bad...Indeed we never thrown NullReferenceExceptions for null argument values. This is why I just corrected it. However regarding the second point I have a different opinion. Specifically if you don't want your program to crush unexpectedly, whenever a possible exception may be thrown, you have to catch all exceptions and act appropriatelly. I exclude from this cases exceptions like null argument exceptions which indicate bad usage of a method for instance. – Christos Sep 24 '17 at 04:53
  • So since many exceptions can be thrown from a call and we might not want to handle only one of them in a specific manner, I think that the only way to cacth them all is to cath the most generic exception. Otherwise definitely can add some extra catch clauses for the exceptions we want to handle a bit differently and leave the most generic as the final catch statement. This is my 2 cents on this topic and by no means I argue that this should be followed or is the most correct one :). – Christos Sep 24 '17 at 04:57
  • @Downvoter I would appreciate your reasoning. Thanks in advance ! – Christos Sep 24 '17 at 14:37
  • Not the downvoter (yet), but logging an exception is not handling it, and neither is returning null. The method is "Load, not "TryLoad" or "LoadOrNullAfterLoggingException". Having Load swallow exceptions increases the error handling code that has to be written AND you are throwing exceptions in a method where you are swallowing all exceptions, which means that the caller has to do even MORE. Your first 2 sentences are correct and directly answer the question, everything else goes off into left field. If you wanted, you could add "and obj doesn't get assigned if the cast or Deserializer fails" – jmoreno Jan 01 '18 at 07:14
  • @jmoreno I agree with you on the naming of the method. But my approach it would be, if OP goes to use the above answer, he should rename the method to `TryLoad` or `LoadOrNullAfterLoggingException`. Definetely this would make the method more honest to their callers. Good catch ! On the other I dissagree with your view on the matter of logging the exceppion and returning `null`. IMHO, which in **no** way means it is correct -just 2 cents-, I find more approriate to return null, which wull denote that something went wrong and log the exception in order to help any later investigation. – Christos Jan 01 '18 at 15:57
  • Null has one meaning, no value, don't try to redefine it as an error. It can be an error condition, just as having a birthdate of next year, but that is context dependent. If the method is TryLoad it should NEVER throw. The contract implicit in the name is for it to Try, the only possibly exception would be NotYetImplemented, why it fails is not part of the name. Exceptions should transfer flow from a method that cannot fulfill its contract, to a method that can. Logging is orthogonal to that, where to log an error depends upon where the best understanding of the error can be obtained... – jmoreno Jan 01 '18 at 16:39
  • @jmoreno I didn't try to redefine null as an error. I just picked up this value as the value that would be returned if the deserialization fails to denote that something wrong happened. Furthermore, I haven't mentioned that `TryLoad` or `Load` would throw any error. This is why I have chosen a try/catch inside the method. Doing so the consumers of the method woulnd't have to think about any exceptions. They would except a value or null. If we remove try/catch from the method's body and this methods is called in more than one place, then this logic should be transfered more than one times. – Christos Jan 01 '18 at 16:46
  • @jmoreno When someone calls this method excepts a value not an exception, which when it will not be handled the program will crash. So we return always a value and null when we didn't make it to deserialize properly the requested item. This way we guarantee always a not null value or a null value when something goes wrong. Once more I note that this is MHO. I don't say that's the correct and all the other opinions are wrong. – Christos Jan 01 '18 at 16:49
  • ...while my opinion isn't a law of nature, I do believe it is 100% correct, and as with someone providing an answer that allows sql injection, I tend to downvote answers where I see recommendations to do what I perceive as incorrect. TryLoad, no exceptions would be fine (with or without the extra parameter). Load with both exceptions and null? -1. On a related note, named tuple deconstruction makes a different pattern more accessible, which I wouldn't normally use but you might consider: return (result, error). Your contract then explicitly states that you are returning an error. – jmoreno Jan 01 '18 at 16:58
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/162291/discussion-between-christos-and-jmoreno). – Christos Jan 01 '18 at 17:01