Not just collections, you should stop returning null for everything, really. If something might not exist, you should wrap it in a Result or an Option. This way, the type system checks your code for null accesses and you will get a compile-time error*. Sometimes, empty containers introduce unexpected bugs also. *In most cases, C#'s type system isn't sophisticated enough to guarantee it in all cases. F# is much better in this regard.
I don't think I would agree with this approach. You have only traded one invisible error for another. If the consumer is asking for something, receiving an empty list does not communicate that the item did not exist. It implies that it does exist but is empty. null on the other hand clearly states that the item is non-existent. It's the whole point of null. You're not removing that null check code, you likely would have to replace it with `if (list.isNotEmpty())` and still have the extra code. Sometimes this may not be an issue if the consumer noops on an empty collection, but that's not always the case. Rust definitely does this better by forcing everyone to always check for fail states. They will return either an Option in place of null, or a Result where instead of just `null` it also carries an Error to describe why it is null. I agree that we should continue moving away from null, but not towards a blanket approach to empty collections like this because you lose a lot of meaning and can no longer handle errors properly. You just get a silent failure instead.
The point is that you're asking for a collection. In this case, if no item matches your description, you should get an empty collection. Returning null is then simply another way of saying empty array. I dont think (vs as stated in the video) you're getting rid of an empty list check. If you need to handle that case, you have to handle it anyways, you just don't have to handle the null check as well.
Unless your language also supports a type like "at least one element array" like typescript does, then it doesn't really matter which method you choose, as you have only one way of saying "empty array"
@@deluksic for a real world example, I have been struggling with Redis lately because it follows the practice of returning empty collections when an entry doesn't exist. I have had to add extra code to try and determine if the collection exists or not because I need to return a 404 if it is invalid. Another example, I am looking up a user's transaction history in a database and I need to know if the user does not exist, or if their history is just empty. Following this person's advice, I can no longer determine that from his API. Null was created for a very good reason, it just implemented terribly. We will always need the concept of "nothing", whether it be null, or None in Rust. In the current setup he has, the only way to communicate that non-existence is by throwing an exception, which is way worse than just returning null. This is also why moving from Java to Kotlin, or C++ to Rust, or Obj-C to Swift, has become so mainstream. They help to enforce the handling of null types. I rarely if ever get NPE in Kotlin, and almost always because of JVM level issues that can circumvent Kotlin's null checks.
@@deluksic The point I'm making is that there is a difference between having an empty collection, and having nothing. If I make a request for a collection, but the user I requested doesn't exist, I should receive a "nothing" response. If you only return an empty collection, I don't know that the user doesn't exist, instead I have to assume the user does exist, but their collection just happens to be empty. Null was created very a very good reason, and every language has a notion of "nothing". In languages like Kotlin, Java, C#, JavaScript it's null. Python has None, Lua is nil. Rust has the Option type with Some() or None. The point is that you need to be able to provide a "nothing" value, and complaining about having to add a null check in your code everywhere doesn't fly. The problem with null in Java/C#/JS/C++ is that it is terribly implemented. It's the whole reason why languages like Kotlin/Swift/TypeScript/Rust were created, to provide that null safety. I do Kotlin everyday, and the only null errors I get are only when I'm fighting the JVM itself which can circumvent Kotlin's null safeties at runtime.
@flamewave000 Yeah, let's say you want to get posts created by a user. If that user does not exist, maybe you should "throw" instead? Would you rather return 200 OK, null? I agree that you would want to differentiate this case in some way.
You can also apply this to string-type return values, where you can return String.Empty. Although with strings its a bit more tricky, since you have to think about the semantics of what an empty string actually means. Do you have some reason to distinguish an 'intentionally' empty string vs. a value that was not provided? Most of the time I do not, and I prefer to return an empty string.
@@megatronusv2215 this is very true. There's a difference between requesting a collection and it existing but is empty, and it NOT existing at all. If you return an empty collection for both, then you prevent a consumer from being able to handle that non-existent error case. Instead you would be forced to throw an exception, which is expensive and in languages like Java, there's no way the consumer to know what kind of exceptions a function might throw. So much better to communicate errors through the result, and not through exceptions.
Dumb advice, born out of people that can't check their calls and consume blindly. Null is indicator of an ERROR, not an empty anything. xEmpty is the return of x that had nothing. If you follow this idiocy, people loose the ability to tell whether something really had no results to return or some error occurred. If you want to be even more thorough, you create a message passing class that not only returns if the call was successful along with the returned object or that it was not, with a proper error code. This is very inline with HTTP. The result of "page not found" IS NOT an empty page, it's HTTP 404 - Page not found.
First, your example code would not compile, since it lacks the nullability indicator, which is on by default. Second, don't ever return IEnumerable unless you have a very good reason to do so (which nobody ever has, unless you are actually streaming back data,but then it would be IAsyncEnumerable instead anyway). Always return IReadOnlyCollection or IReadOnlyList, since you anyway already have materialized the results, and allowing the consumer of your api to check for the amount of data returned makes their life easier, especially since they can safely call .Any() or .Count.
How does using a dynamically typed language 'fix' this? That just converts a compile time error to a potential runtime error that's harder to debug. if it doesn't error, it's because a hidden, implicit type conversion arbitrarily decided by the language's creator prevented it.
@@sfertman I work every day in many many languages (Kotlin, C++, Rust, GoLang, JavaScript, TypeScript, Lua, and Python just in the past 2 months alone). I can tell you right now that the easiest by far to debug has been Rust. The stronger the typing and the stricter the compiler the far fewer "dumb" mistakes a developer can make by accident, and creates high predictability in code. I spent 2 hours writing code in Rust and when I could finally compile it, it ran exactly as I wanted without ANY errors or problems. I can spend the same 2 hours writing Kotlin and have it not run perfect and have to be debugged. JavaScript is a hell-hole, it's so loosey goosey that I have had to spend hours trying to figure out why I had problems. You also have to rely so heavily on online documentation when using npm packages because the code has no typings and VSCode is rarely able to determine what a function returns, or what the types for a function parameters are supposed to be. No, the only purpose for dynamic languages is to fail forward and be compact. Which is exactly what JS was designed for. Hell even Python 3.5+ (another language previously notorious for dynamic types) started including enforced type hints as part of the language.