Throwing an exception when necessary is part of the constructor’s job.
Consider why we have constructors in general.
One part is the convenience of using a method that sets various properties and possibly performs some more complex initialization (for example, FileStream will actually access the corresponding file). But then, if it were always really convenient, we would sometimes not find convenient member initializers.
The main reason for constructors is that we can support invariants of objects.
An invariant of an object is what we can say about the object at the beginning and at the end of each method call. (If it was designed for simultaneous use, we would even have invariants that persisted during method calls).
One of the invariants of the Uri class is that if IsAbsoluteUri is true, then Host will be a string that is a valid host (if IsAbsoluteUri is false Host can be a valid host if it is related to a scheme or is accessible to it, an InvalidOperationException may occur).
As such, when I use an object of such a class, and I checked IsAbsoluteUri , I know that I can access Host without exception. I also know that this will really be the host name, and not, for example, a short treatise on medieval and early modern beliefs in the apotropic qualities of bezoars.
Well, that’s why some code that places such a treatise is not quite likely there, but of course there is some code that puts some kind of garbage in an object.
Maintaining an invariant boils down to the fact that combinations of values that an object has always make sense. This should be done in any device or property setting method that mutates the object (or making the object immutable, because you can never have the wrong change if you never had any changes), and those that originally set the values, i.e. in the constructor.
In a strongly typed language, we get some kind of check for this type of security (a number that should be between 0 and 15 will never be set to "Modern analysis has found that bezoars do indeed neutralise arsenic." Because the compiler just won't let you do this .), but what about the rest?
Consider the constructors for List<T> that take arguments. One of them takes an integer and accordingly sets the internal capacity, and the other a IEnumerable<T> that the list is full. The beginning of these two constructors:
public List(int capacity) { if (capacity < 0) throw new ArgumentOutOfRangeException("capacity", capacity, SR.ArgumentOutOfRange_NeedNonNegNum); public List(IEnumerable<T> collection) { if (collection == null) throw new ArgumentNullException("collection");
So, if you call new List<string>(-2) or new List<int>(null) , you get an exception, not an invalid list.
It is interesting to note that this is the case when they can have “fixed” things for the caller. In this case, it would be safe to consider negative numbers as the same as 0 , and null enumerations as the same as empty ones. They decided to quit anyway. Why?
Well, we have three cases to consider when writing constructors (and even other methods).
- Caller gives us values that we can use directly.
Eh, use them.
- Caller gives us values that we cannot use at all. (for example, setting the value from an enumeration to undefined).
Define an exception.
- Caller gives us values that we can massage into useful values. (for example, limiting the number of results to a negative number, which we could consider equal to zero).
This is a more complicated case. We need to consider:
Is the meaning unique? If there is more than one way to look at what it is "really", then throw an exception.
Is it likely that the caller arrived at this result in a reasonable way? If the value is just stupid, then the caller supposedly made a mistake by passing it to the constructor (or method), and you do not make it any advantage in hiding your mistakes. First, they are likely to make other mistakes in other calls, but this is the case when this becomes apparent.
If in doubt, make an exception. First, if you doubt what you should do, then most likely the caller has doubts about what they should expect from you. For another, it’s much better to go back later and turn code that turns into code that doesn't turn code, that doesn't throw into code that does, because the latter will be more likely to turn working applications into broken applications.
So far, I have only considered code that can be considered validation; we were asked to do something stupid, and we refused. Another thing is when we were asked to do something reasonable (or stupid, but we could not find it), and we could not do it. Consider:
new FileStream(@"D:\logFile.log", FileMode.Open);
There is nothing invalid in this call that must fail. All validation checks must pass. It will hopefully open the file in D:\logFile.log in read mode and provide us with a FileStream object through which we can access it.
But what if there is no D:\logFile.log ? Either there is no D:\ (this is the same, but the internal code may end differently) or we do not have permission to open it. Or is it blocked by another process?
In all of these cases, we are not doing what is set. It is not good to return an object, which is an attempt to read a file that will fail! So again, we make an exception.
Good. Now consider the case of StreamReader() , which takes a path. It looks a bit like (adjusted to exclude some indirectness for example):
public StreamReader(String path, Encoding encoding, bool detectEncodingFromByteOrderMarks, int bufferSize) { if (path==null || encoding==null) throw new ArgumentNullException((path==null ? "path" : "encoding")); if (path.Length==0) throw new ArgumentException(Environment.GetResourceString("Argument_EmptyPath")); if (bufferSize <= 0) throw new ArgumentOutOfRangeException("bufferSize", Environment.GetResourceString("ArgumentOutOfRange_NeedPosNum")); Contract.EndContractBlock(); Stream stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, DefaultFileStreamBufferSize, FileOptions.SequentialScan, Path.GetFileName(path), false, false, true); Init(stream, encoding, detectEncodingFromByteOrderMarks, bufferSize, false); }
Here we have both cases where a throw can occur. First, we received confirmation against fictitious arguments. After that, we call the FileStream constructor, which in turn can throw an exception.
In this case, the exception is simply allowed to pass.
Now the cases that we need to consider are a bit more complicated.
In most cases, the checks discussed at the beginning of this answer did not really matter in which order we did something. Using a method or property, we need to make sure that we either changed the situation to be in the right state, or selected an exception, or left things alone, otherwise we can still get the object in an invalid state, even if this exception was (at most it’s enough to complete your entire test before changing anything). It doesn't matter with the designers in what order things are done, since we are not going to return the object in this case, therefore, if we throw it away at all, we did not add any unwanted messages to the application.
However, when calling new FileStream() may be side effects above. It is important that it be attempted only after any other case has been made that would throw an exception.
In most cases, this is easy to do in practice. Naturally, first put all your checks, and all you need to do in 99% of cases. The important case is that if you get an unmanaged resource during the constructor. If you make an exception in such a constructor, this will mean that the object was not created. Thus, it will not be completed or deleted, and therefore an unmanaged resource will not be released.
A few recommendations to prevent this:
Do not use unmanaged resources directly. If at all possible, work through managed classes that wrap them, so there is a problem with this object.
If you need to use an unmanaged resource, do nothing else.
If you need to have a class with both an unmanaged resource and a different state, then combine the two rules above; create a wrapper class that uses only an unmanaged resource and uses it in its class.
- Even better, use
SafeHandle to hold a pointer to an unmanaged resource, if at all possible. This is very important for working on paragraph 2.
Now. How about throwing an exception?
We can do this. The question is, what will we do when we catch something? Remember that we must either create an object that matches what we asked for or throw an exception. In most cases, if one of the things we tried to do during this crash has nothing to do with successfully building an object. This is probably why we are simply allowing the exception to pass or throwing the exception, just to distinguish another, more suitable from the point of view of someone calling the constructor.
But, of course, if we can consciously continue working after catch , then this is allowed.
So, in everything, the answer to the question "Can you use a throw or try to catch inside the constructor?" "Yes".
There is one fly in the smear. As you can see above, the great thing about throwing inside the constructor is that any new either gets a valid object or throws an exception; there is no between them, you either have this object or not.
The static constructor, though, is the constructor for the class as a whole. If the instance constructor fails, you do not get the object, but if the static constructor fails, you do not get the class!
You are pretty much doomed to any future attempt to use this class or any of its derivatives for the rest of the life of the application (strictly speaking, for the rest of the life of the application domain). For the most part, this means that exception from the static class is a very bad idea. If it is possible that something attempt and failure can be attempted and successful another time, then this should not be done in a static constructor.
The only time you want to drop a static constructor is when you want the application to completely fail. For example, it is useful to drop into a web application that does not have a vital configuration setting; Of course, it is annoying that every single request fails with the same error message, but that means you are sure to fix this problem!