Комментарии:
I like the way Zig does it where the type signature of every function says what it can return (It does not throw exceptions, it returns them as values).
It also will not compile your program unless you handle every type a function can return.
If you are searching for something, but you didn't find anything, returning NULL as being "nothing" is totally correct ! There was no book with that title for example. And yes you can also not use any property of something you did not find in the first place !... Not convinced ? go to a bookstore, ask for a title they don't have , then ask for the price and look at the face of the bookseller ;-)
ОтветитьVery easy to make everything nullable and end up having to handle it everywhere.
ОтветитьUsually when people talk about avoiding null, they just replace it with a meaningless replica which just hides the null and covers it with something more "Pretty"; for example Option<T>.
At the end of the day, whether you prefer using Nullables or Options is just a matter of syntactical preferences.
But this, this is great advice.
Instead of simply side-stepping the null-problem, the Null Object Pattern actually addresses the issues caused by Null Refs; so that, as you said, the pressure is on the Producer of the method, not on the consumer.
I've known about the Null Object Pattern for a while, but I have to say that the best explanation of it that I've seen anywhere, was your video.
Thank you very much for always putting out some of the greatest Programming content on the platform.
In my opinion, C# is sorely missing what Kotlin has implemented in its sealed classes, which are de facto algebraic datatypes. One can mimick this approach in C# as well, but the implementation is not error-prone. At least I did not find a way.
ОтветитьNull reference issues are a legacy from the past. Soon there will be no more reason (such as performance) to avoid making functions tolerant to null references. Just wait a bit for photonic computing.
ОтветитьI think a better title for this video would be "never return a null collection". Applying this idea to individual objects seems like it would be a nightmare
ОтветитьGreat video. I have a lot of practicing to do with this concept. I remember reading about it in one of Bob Martin's books, but never quite understood it until I watched this video. The example with the empty list is much cleaner and means you don't have to change much. Everything after .GetDiscounts() still runs as normal but doesn't worry about running into any nulls (it's like running filter() on an empty array).
To make sure I full understand this, what could we do to dynamically apply "no discount" to a price? My thought would be to create a sort of "base" class and extend it into the NoDiscounts class. Then, when the logic comes to where it discounts the price, you create an instance of NoDiscount and apply your operations to that. Otherwise, create it as the regular discount class. Any thoughts on this? I'm sure there's an easier way. I still have a long way to go before fully understanding OOP.
Thanks for posting this, and have a great day! You earned a new subscriber.
Thank you Zoran. This is a very useful technic. I would really appriciate if you could share in some video when to use Null object VS Option. Sometimes it is not so clear (at least to me)
ОтветитьAdding items to the List can be "chained" by writing an extension method, that takes every item of the resulting enumeration and calls List.Add(). You can still use ?. to stop call execution on Null reference. Extension method can be reused in many other places. NullDiscount type is very specific and brings no value to the domain, just pollutes it.
Ответитьalso can apply for c++
ОтветитьWhat I don't like about this design is that the consumer now has to know about this NoDiscount class in order to check the empty case. In fact, every domain model class which otherwise might not have needed an interface will now need one so that a "No X" class can be made for each one. Instead of just returning, say, Location?, now we need Location : ILocation and NoLocation : ILocation, and the caller has to know about both. I'm not sure it's worth it.
ОтветитьIncredible video ! I am also a fan of error as value that really belong to a specific type. Some functional language implement this functionnality by default but the null object pattern seem super powerful !
ОтветитьThis is not necessarily a good idea. This is the Null object pattern. The problem with this pattern is that you are just passing the buck, i.e. if you don’t have a discount then you DONT have a discount. The immediate calling code in this example doesn’t care but code later might break. Let me give you an analogy: you are sick so call a doctor. A doctor isn’t available so instead a random person from the street is sent (the null object). You don’t know this and the “doctor” has a great bedside manner (acts like a doctor). But he injects you with the wrong thing and you die ;). The upshot is that is you have a situation where a discount isn’t available, you can’t hide it. In the example caller, I would probably use nullable but then use .e.g. .OfType<IDiscount>() to filter out null results.
ОтветитьI like to learn from you, as your a uni teacher that i never had
ОтветитьGood video. The concept of null sometimes serves a purpose in low level code with pointers, but If you do not have a need to deal with null explicitly it's better to avoid it. The null object pattern is the way to go when you have multiple implementations of an interface and want one to do nothing.
ОтветитьGood video, so dramatic
ОтветитьI've always liked this way of handling things if given the opportunity in the code base and if it also applies to the language I'm working with.
What is your suggestion for intrinsic object types in this regard?
This approach makes sense in some cases, such as when you're returning a collection as you are here. However, if you are returning one object, I still think null is simpler, and it can be faster as you don't need to allocate anything to return to handle this case.
ОтветитьThis is stupid. It doesn’t serialize null to your fake null substitute. And it is worse when you get to an integer. 0 is NOT a substitute for null. Neither is int.max or int.min
Null means the value doesn’t make sense. So short circuit or throw an exception. Stop writing g bad code.
Also, change ALL warnings to errors in your project file. If you want to do as this video describes, switch to visual basic with its default weak duck typing and ambiguous assumptions
# end rant
simple and useful 🙂
ОтветитьWhat if caller needs to behave different? I often see code like if(result == default) which is not so different to if(result == null). probably better to look into returning option.
ОтветитьExcellent lesson
ОтветитьHow about using your useful Option<T> which inherits IEnumerable<T> and contains or not only one element with Some or None methods? Very simple and cool idea. Thanks
ОтветитьAs soon as I came into contact with the concept of a null reference I found myself puzzled as to why it was even exposed to the developers of C# in the first place. It means null, the void of space, the absence of value in the end, the pure nothing so why should I use it? I see no use case and avoid it in my projects and in my work as much as I can or are allowed to do.
Consuming null introduces a whole set of ordeals, considerations, test scenarios and so on which give less value that it asks from you when you start using it.
Using null to return when nothing applies is borderline unconsiderate with the consumers of your code or library and has very few use cases, even if you are the mantainer of the code, you should return something that signifies "This value is not valid".
I applied this same concept to enums as well, which by default return the value of 0 and the first member, if not assigned to any other value, will always be 0. I have a funny story regarding that and an exception of a user validation in a company I joined some time ago. The value of 0 meant "Authentication successful", guess what happened when an exception arose and a default object was returned with errors. The client was given a value that meant "go ahead, you're clear".
There are so many things you have to know about null and default values to do things in a better way, that it becomes daunting.
"null reference exceptions are almost impossible to cause nowadays" - Cannot convince my company of this, who have strict policy on using anemic objects, and turn off nullable compile warnings in all projects because it "takes away needed flexibility". If there are any warnings left, they just put exclamation mark there and fix it when users report bugs. T_T
ОтветитьIm waiting for the FP fanboys to come and say that functional is better 😂
ОтветитьI'm using OneOf library to not return null's
ОтветитьUseful video, as always!
After seeing this and your video about monoid, I would love to see a comparison between the two, when should one use one over the other with use cases.
Thank you!
Nowadays I just use Option
ОтветитьI just use `Optional` as every 'clever' abstraction over a lack of a value (null object, etc) all leak and cause issues. You almost always have to check for the 'sentinel' value at some point or its completely hideous to support all the boilerplate to avoid it.
Optional (and similar containers) let me focus on the logic vs the machinery (loop counters, flags, nulls, threads)
I use typescript, not C# and I use the nullish coalescing operator a lot. With GraphQL, the client often fetches different data for the same object. Therefore when I'm making a reusable component, it needs to accept "partial" objects, which may not have these properties. Obviously we don't want that to happen, but the null chaining and ?? operators help me prevent runtime errors.
In other places, null is simply more accurate. If I'm fetching the prices for a given product, I want the state to be {price: null, priceFetching: true}. State with price 0 would mean something different. This allows me to discern between the price existing/being loaded and it just being 0.
It feels better to consume resources that do not return null, but such code also introduce extra memory overhead that may not be suitable for high throughput applications.
We're substituting checking for a null pointer with dereferencing a newly created heap object that needs to be garbage collected (Depending on what you're returning).
From what I know, Enumerable.Empty<T> is fairly performant as it'll create a new collection for each type that can be reused during the lifetime of the application, but for any other reference type, instead of checking the pointer, you're creating and pointing to a new heap object that needs to be garbage collected, and it has to dereference that object instead of just checking the pointer, every time.
I just see the new object being allocated each time and see a possible terrible waste of resources, of course depending on the application.
I like the idea but probably wouldn't apply this in every scenario.
Suppose if method requires valid domain object, and then we pass in some object that is not null, but represents a concept of null object (follows null-pattern).
Point 1:
That object doesn't exist in domain model because object of NullClassEntity is not value absence, but entity of concrete class (think of it as, for example, classA and classB both allow value absence, but we should create two NoClassValue for each of them to accommodate pattern requirements).
Point 2:
What should method that recived such an object do? Is it special check like if (obj is type) or something? This check will pollute code I think (in comparison to .? or ?? operators).
As an example:
We have function that maps one set of objects to another and from this perspective map from objectA to null is valid and totally acceptable even in math.
So doesn't that pattern look old in modern C#?
The example given I agree with only because it's internally representing a list of things and it's always preferable to return an empty list than a null. Anything more and it invokes my PTSD from using DBNull.Value before I made extensions to translate to/from null automatically. At any boundary of your code a null object will need to be translated into a null for sanity reasons. Any API enpoint or serialization would be worse off if you have to deal with a null object in the data. It feels like the pattern is just shifting the problems to the peripheries.
ОтветитьNull is a really interesting beast in OOP. Developers embrace the value object pattern in domain modeling, talk how it's important to avoid primitive obsession, yet nulls often slip through. "No value" is a value and it should definitely be represented with a concrete object. Not only this removes the burden of checking for null by plastering question marks everywhere, but also keeps domain code clean and explicit. And another important thing: an "empty value" object preserves the intent when null has no information attached to it. Because of that null may mean no discount, lost service connection, missing database entry, parsing error, or whatever. Rebuilding my pet project in F# literally turned around my way of thinking about domain logic implementation and coding in general. Now I use Option<T> and Resut<TResult, TError> even in PHP (with the help of phpdoc magic), and throw exceptions for the actually exceptional circumstances that put my app into crashed state. The only place I now usually allow myself to return null is a React component
ОтветитьI do not like this new craze of ditching all nulls. There is always that golden center. Extreme approaches should be avoided by definition.
In cases like GetDiscounts we should get an empty collection in case there are no discounts. It is natural, and returning null here is a mistake, IMO.
Then there are cases like GetDiscount/GetCurrentDiscount - here we want only one instance of the class. We can return null or NullValueObject/NeutralValueObject. For discounts, it could be an object with a 0% discount value. Then we just process it and do not have to worry whether it is null or not.
But there are cases where we're going to end up with checks against that NullValueObject anyway, eg. if (x is EmptyValue). That is no different from checking if (x is null) IMO. We have to check anyway, and not having a null reference exception can make this error harder to notice.
Great video! I personally used to use null or default returns often, but over time they just make code more complex and almost guarantee that you will get a null at some point which you have to handle in ALL consumers. I think I ended up using null coelescing operators more as a bandaid here for bad code. Never good when your code has more questions than the person reading it does.
To move away from nulls I did opt for a result wrapper type after seeing it used a lot in the community, which works well when used appropriately. But lately I prefer to use a concrete "invalid" instance of the type as this felt much cleaner. Pretty much exactly what you're doing here with the NoDiscount object.
The Unity Game Engine overloaded the == equality operator for all their Object types to consider the Null object equal to the C# null value, and it was a very poor design decision, which some of their developers say that if they started from scratch they would not have done it that way. The approach in this video only works well if the class upholds every part of the contract, preconditions, postconditions, class invariants, and side effects that are guaranteed by the interface. The video basically just re implemented whatever concrete type was returned from Enumerable.Empty<T>() , which works because it upholds the contract of IEnumerable which is guaranteed to have zero or more items (i.e. "there may be no discounts" was already implied by IDiscount).
ОтветитьExcellent video, the concept is crystal clear. In fact, it's more expressive and you can specify more than a null object in a simple way. In that way you can add discount operations like add discounts together etc.. that will be a pretty cool video after this one, some custom operators with discounts.
Good job as always! 🚀
When returning Null we usually do ourselfes disservice, yes it's easier, we just skip a part of domain logic, throw it into a black hole - Null. But sooner or later we have to payoff for that decision when our app crashes in the runtime. So it's alway a good idea to invest into domain modeling and define explicitly what Null means in the context and the Null Object pattern comes to the rescue.
Thanks for the content, Zoran, as always! 😊
First I thought I will use my own generic option/optional/maybe type in situations like this. But then I had the idea to use nullable references as option/optional/maybe type through extension methods (like adding the extension method "ValueOrDefault"). I think it's cheap and fits better to the language. But I didn't have the opportunity to test this in big software.
Ответить