𝄯 Sharp Regrets: Top 10 Worst C# Features



  • You can fall through. There just can't be any statements in all but the last branches.


  • Banned

    @Maciejasjmj said:

    How would you determine the lambda type?

    The same way every other language does.

    @Maciejasjmj said:

    Even assuming you specify the lambda argument types directly

    Why should I? Doesn't C# have generics?

    @Maciejasjmj said:

    you still don't know whether the user wants a DIY delegate type, a Func<>, an Expression<Func<>>, or some other type assignable from a lambda I've probably missed

    All function types should coerce to all other function types with same signature.


  • Banned

    @Maciejasjmj said:

    You can fall through. There just can't be any statements in all but the last branches.

    Correction: C# does indeed have fall-through, but it's utterly broken.



  • @Gaska said:

    Doesn't C# have generics?

    Yes. How do you infer the type for the generic?

    var myLambda = x => x.Value

    Is it Func<int?>? Func<Nullable>? Func<SomeRandomTypeWithAValueProperty>?

    @Gaska said:

    All function types should coerce to all other function types with the same signature

    So you want C# to be a duck-typed language? Not to mention Expressions, while they can be assigned to from lambdas and compiled to functors, are not functors themselves.



  • @Gaska said:

    C# does indeed have fall-through, but it lets you do it in the only way that's not shooting yourself in the foot

    FTFY


  • Banned

    @Maciejasjmj said:

    Yes. How do you infer the type for the generic?

    var myLambda = x => x.Value


    You can't. But inferring type of var myLambda = x => x is trivial - Func<T1, T2>, with actual T1 and T2 filled in when the lambda is actually used.

    @Maciejasjmj said:

    So you want C# to be a duck-typed language?

    No - I want to use my function wherever I can use a function of same type.

    @Maciejasjmj said:

    Not to mention Expressions, while they can be assigned to from lambdas and compiled to functors, are not functors themselves.

    Why can they be assigned from lambdas if they're not functorsfunctions? And what's the practical difference between being compiled to functors and being functors?

    @Maciejasjmj said:

    C# does indeed have fall-through, but it lets you do it in the only way that's not shooting yourself in the foot

    The only way to use fall-through without shooting yourself in the foot is to not use fall-through. And C# does exactly that, which is good. But why lie that C# has fall-through when it doesn't?



  • @Gaska said:

    But inferring type of var myLambda = x => x is trivial

    At best you can infer an incomplete generic type, which you can't instantiate or have a variable with.

    Also:

    var myLambda = x => x;
    if (someCondition) 
        var y = myLambda(someInt) 
    else
        var z = myLambda(someDateTime) 
    

    What can you call myLambda with after such code block? What type is myLambda before that block, what type is it after that block, and how did the variable type change during program execution?

    @Gaska said:

    I want to use my function wherever I can use a function of the same type

    Actually you can, I forgot that. A delegate variable of a type that takes an int and returns a string can be assigned to from any function that takes an int and returns a string.

    You just need to know what the "same type" is.

    @Gaska said:

    And what's the practical difference between being compiled to functors and being functors?

    The same as between your C source file and your executable.

    You can parse Expressions, and often you don't actually want to compile them to callable objects (with, say, LINQ to SQL). And I think you can have Funcs which aren't created via expression trees, but I'd need to check that.

    @Gaska said:

    But why lie that C# has fall-through when it doesn't?

    case 1:
    case 2:
    doStuff();
    break;
    
    case 1:
    break;
    case 2:
    doStuff();
    break;
    

    Both are valid C#, both are occasionally useful, both do pretty much the exact opposite things.


  • Banned

    @Maciejasjmj said:

    At best you can infer an incomplete generic type, which you can't instantiate or have a variable with.

    You could with better inferrence. Sadly, C# cannot look forward in the code to determine what type you want.

    @Maciejasjmj said:

    The same as between your C source file and your executable.

    You keep Expressions in git and make lambdas out of object files?

    @Maciejasjmj said:

    You can parse Expressions, and often you don't actually want to compile them to callable objects (with, say, LINQ to SQL).

    What is this Expression anyway? Why it's good idea to assign lambda to Expression variable if it's not a callable type?

    @Maciejasjmj said:

    Both are valid C#, both are occasionally useful, both do pretty much the exact opposite things.

    And neither exercises the fall-through property of C switches. You know full well what I mean so stop fucking around with bullshit definition of fall-through that nobody uses.



  • @Maciejasjmj said:

    So you want C# to be a duck-typed language?

    You can actually have implicit typing in a strongly-typed language. Here's a Nemerle example:

    using System.Collections.Generic;
    
    def dict = Dictionary();
    dict["key"] = 1; // ok, so dict is a Dictionary[string, int]
    
    dict[1] = "value"; // won't compile - dict is a Dictionary[string, int]
    

    And the error message is pretty clear: int is not a string, so...

    type.n:6:1:6:8: error: in argument #1, needed a string-, got int: System.Int32 is not a subtype of System.String [simple require]
    


  • Except what if you actually wanted a dictionary of object? Now that's an intentionally bad example. The actual use case it's when you want a dictionary of some useful type T, but the first thing you put in happens to be a subtype of T—in a rooted language, you can't do the right thing there without completely breaking type interference—everything would always infer as object.



  • Traditional languages are easier to write than they are to read. C# design explicitly tries to shift the balance in the opposite direction.



  • @Buddy said:

    Except what if you actually wanted a dictionary of object?

    You mean, something like this?

    using System.Collections.Generic;
    
    def dict = Dictionary.[object, object]();
    dict["key"] = 1; // ok, so dict is a Dictionary[object, object]
    
    dict[1] = "value"; // ok, so dict is a Dictionary[object, object]
    

  • Banned

    @Buddy said:

    Except what if you actually wanted a dictionary of object? Now that's an intentionally bad example. The actual use case it's when you want a dictionary of some useful type T, but the first thing you put in happens to be a subtype of T—in a rooted language, you can't do the right thing there without completely breaking type interference—everything would always infer as object.

    And that's why rooted type system sucks.


  • Banned

    @Buddy said:

    Traditional languages are easier to write than they are to read. C# design explicitly tries to shift the balance in the opposite direction.

    What exactly do you mean by traditional languages?



  • @Gaska said:

    Sadly, C# cannot look forward in the code to determine what type you want.

    Again, what about branching? Or do you want to have a runtime type exception when your inference doesn't work, which pretty much counters the point of a strongly-typed language?

    @Gaska said:

    What is this Expression anyway? Why it's good idea to assign lambda to Expression variable if it's not a callable type?

    To have it parsed, not called. Expressions are object representations of expression trees, so x => x.Prop1 + x.Prop2 will be a LambdaExpression with Body of type BinaryExpression where Left and Right sides are MemberExpressions, etc., etc.

    It's especially useful with LINQ-to-SQL - instead of fetching everything from the database and calling the lambdas on the collection, it parses the lambdas and translates them to SQL which is then emitted to the DB. Actually compiling an expression is unnecessary and gives you a perf penalty for no benefit.

    Also it enables you to have property selectors in your own code without resorting to string typing - if your function takes an Expression<Func<>>, you can pull the name of the property to pass to the reflection APIs from it rather trivially, but renaming said property will hit you with an error at compile time.

    @Gaska said:

    And neither exercises the fall-through property of C switches.

    How so? It falls through the cases. There are no statements being executed because C# doesn't let you put them there, but it's still fall-through.

    Obviously it's replaceable with case 1,2: in this case, but you can thank C for the kludgy syntax.

    @Gaska said:

    And that's why rooted type system sucks.

    So you'd rather there were no way to just pass "any object" to a function? Generics usually cut it in such cases, but not always.


  • Banned

    @Maciejasjmj said:

    Again, what about branching?

    All branches must result in the same type inferred. Sheesh, have you ever used any language besides C#?

    @Maciejasjmj said:

    How so? It falls through the cases. There are no statements being executed because C# doesn't let you put them there, but it's still fall-through.

    If it doesn't let you put any statements there, it's not fall-through but a single case with two entry conditions.

    @Maciejasjmj said:

    So you'd rather there were no way to just pass "any object" to a function? Generics usually cut it in such cases, but not always.

    In 99,999% of cases, "any object" is wrong thing to do. The remaining 0,001% is usually covered by another language feature - C++ has boost::any, Rust has Any trait, and the rest of languages I haven't used, but they surely have something.



  • @dkf said:

    Were you doing it by writing the code using an ordinary function that just returned the next value, or were you doing it by managing the stack context such that you could write code with the same sort of structure as C# with yield return?

    It was definitively not an ordinary function; I used GOTOs to continue the function where I had left it in the middle before. And the stack was an array.

    @dkf said:

    The whole point is that while it is theoretically possible to do the transform by hand — there's no theoretical increase in capability from the functionality — it's actually really annoying to do in complicated code, and the compiler does a much better job of it,

    To have a compiler do a better job at it, you have to have a compiler (and of the optimising kind, too) first, and not a simple interpreter that is designed to work within 8 KiBytes of ROM machine code and with 4 KiBytes of working storage total.

    It wasn't about performance, it was about getting something done at all. And from the logical / data flow point of view, an equivalent of yield seemed to me the best thing. (Edit: as in, "easier to implement a yield functionality than to rewrite the algorithm)

    My point was that I didn't need to learn the way a function with yield return works. (Only the syntax C# uses for that.)



  • @Gaska said:

    What exactly do you mean by traditional

    Bad.


  • Banned

    You have weird understanding of words.



  • @Magus said:

    trying to build a solar system

    Gathering the raw materials is the hardest part.



  • @Scarlet_Manuka said:

    @Magus said:
    trying to build a solar system

    Gathering the raw materials is the hardest part.

    Perhaps, but getting everything orbiting properly1 and waiting a few billion years for something interesting to happen both present their own challenges.

    1 Too much total angular momentum, and all your raw materials will orbit the center at such great distance that there won't be much interaction between them; too little, and they'll just collapse into a star without much orbiting it — either way, not a very interesting system.


  • Discourse touched me in a no-no place

    @PleegWat said:

    I know in my case I was using an ordinary function, and saved my own state. If you need the actual yield return pattern in C, you'd probably have to switch to a new stack and use coroutines - I don't think you can transform to only save the state and run in the existing stack if the compiler doesn't support that.

    The transform to continuation-passing-style is automatable, but quite annoying by hand. You have to trace the possible routes through the basic blocks from entry points to exits, and you have to collect up all the state that is hanging around. These are things that compilers are very good at, to be honest.

    I used to write a lot of code in cps. It was good as it let me write complicated services that ran entirely single threaded while serving many users at once. (Everything was still I/O-bound of course.) But figuring out how to write the code was sometimes like an exercise in extending chess into 11-dimensional hyperspace. 😵

    C# generators make this sort of thing much easier, and are actually really rather neat. General coroutines are even better; you can hide most of the sneakiness away if you want then.

    @PWolff said:

    It was definitively not an ordinary function; I used GOTOs to continue the function where I had left it in the middle before. And the stack was an array.

    That's actually probably much like the things that are going on at the IL level (or even the native code issuing level) when people do this sort of thing in C#.

    @PWolff said:

    It wasn't about performance, it was about getting something done at all.

    I'm sure there were other approaches. I'm sure the one you chose was better than those. :-)


  • Discourse touched me in a no-no place

    @Maciejasjmj said:

    At best you can infer an incomplete generic type, which you can't instantiate or have a variable with.

    There are languages that do that in a type-safe way, such as Haskell. Indeed, they've actually got stronger type systems than C#.

    @Maciejasjmj said:

    How would you determine the lambda type?

    You have the entire compilation unit class, not just the definition statement. Surely you can use all the information at your disposal? (Though I wouldn't bother going wider than that. It might help, but it starts to become more confusing than useful.)


  • BINNED

    @Gaska said:

    What exactly do you mean by traditional languages?

    COBOL and Ada. 🚎

    @Gaska said:

    You have weird understanding of words.

    Maybe he's going for the "No True Scotsman" badge, which we (un?)fortunately don't have yet.



  • @dkf said:

    There are languages that do that in a type-safe way, such as Haskell. Indeed, they've actually got stronger type systems than C#.

    C# favors simplicity in this case. Type is only inferred from the declaration, not future use. It has some limitations (as noted here), but it is much easier to figure out what's going on when things go wrong. So, it's not about C# being "not good enough" or having a buggy compiler - it's about design choices.


  • Discourse touched me in a no-no place

    @Jaime said:

    Type is only inferred from the declaration

    Strictly, it doesn't just infer the type, but the concrete type. There's a world of difference there.



  • @Ragnax said:

    How are you going to do that without somehow keeping an index?

    Fear will keep the list in line - fear of this battlestationforum!



  • @ScholRLEA said:

    Fear will keep the list in line - fear of this battlestationforum!

    Disconumbering™ will do. Never ascribe to fear what can be explained by Doing It Right™.



  • @Ragnax said:

    There is also no guarantee by foreach that each element will be passed only once. This is a guarantee on the indiviudual collection classes only. You can easily build an implementation of an IEnumerable or IEnumerator that returns each element twice.

    By doing so, you're intentionally violating the IEnumerator<T> contract, which explicitly spells out how Current and MoveNext are supposed to work.



  • @dkf said:

    You have the entire compilation unit class, not just the definition statement. Surely you can use all the information at your disposal?

    Technically, but that's some hardcore spooky action at a distance when removing an assignment to a variable (or worse - a use of the variable, which would be the case with lambdas) somewhere in the code invalidates the typing of the variable.

    It's not that it can't ever possibly be done, but it really can't be done in a simple way (and doubly so for lambdas with no input types specified), and having to know all the in-depth rules to determine whether your functor has a valid type seems much more of a burden than just using explicit type declarations.



  • @powerlord said:

    @Ragnax said:
    There is also no guarantee by foreach that each element will be passed only once. This is a guarantee on the indiviudual collection classes only. You can easily build an implementation of an IEnumerable or IEnumerator that returns each element twice.

    By doing so, you're intentionally violating the IEnumerator<T> contract, which explicitly spells out how Current and MoveNext are supposed to work.

    I didn't find anything specifying that no element can be returned twice there.


  • Discourse touched me in a no-no place

    @powerlord said:

    By doing so, you're intentionally violating the IEnumerator<T> contract, which explicitly spells out how Current and MoveNext are supposed to work.

    That does not say that the collection that you're modelling needs to pretend that it has no duplicates. It's a sequence of things, that is all you need to know and it's cromulent in itself.



  • @powerlord said:

    IEnumerator<T> contract, which explicitly spells out how

    I am srsly jealous of the quality of the C# documentation... coming from R and other even more esoteric stuff... this is pretty nice stuff.

    As a C# noob, "covariant?" .... OH! they explain that briefly, and link, like that new-fangled World-Web-Web-of Hyperlinks thing I've heard about...

    From the linked documentation: (highlighting my own)

    "This type parameter is covariant. That is, you can use either the type you specified or any type that is more derived. For more information about covariance and contravariance, see Covariance and Contravariance in Generics."

    TIL: progress is really happening.



  • @ijij said:

    I am srsly jealous of the quality of the C# documentation... coming from R and other even more esoteric stuff... this is pretty nice stuff.

    The quality of the C# documentation and tool is what makes it so difficult to move on to any other (shitty) language after working awhile in C#.


  • Discourse touched me in a no-no place

    @blakeyrat said:

    The quality of the C# documentation and tool is what makes it so difficult to move on to any other (shitty) language after working awhile in C#.

    Some parts are really good indeed. Other parts make me feel like I'm trying to see the forest while nose-deep in the leaf-litter. Not that they're the only documentation like that, oh no. There's a real tension between getting the details which you sometimes need and the higher-level picture of things so you can fit the pieces together into a solution for your problem.

    But at least MS understand the importance of decent documentation.



  • @PWolff said:

    @powerlord said:
    @Ragnax said:
    There is also no guarantee by foreach that each element will be passed only once. This is a guarantee on the indiviudual collection classes only. You can easily build an implementation of an IEnumerable or IEnumerator that returns each element twice.

    By doing so, you're intentionally violating the IEnumerator<T> contract, which explicitly spells out how Current and MoveNext are supposed to work.

    I didn't find anything specifying that no element can be returned twice there.

    Initially, the enumerator is positioned before the first element in the collection. At this position, Current is undefined. Therefore, you must call MoveNext to advance the enumerator to the first element of the collection before reading the value of Current.

    Current returns the same object until MoveNext is called. MoveNext sets Current to the next element.
    If MoveNext passes the end of the collection, the enumerator is positioned after the last element in the collection and MoveNext returns false. When the enumerator is at this position, subsequent calls to MoveNext also return false. If the last call to MoveNext returned false, Current is undefined. You cannot set Current to the first element of the collection again; you must create a new enumerator instance instead.

    I've bolded the relevant sections. Well, that's not true as everything in this section is relevant, I just bolded the more relevant parts.

    The defined behavior precludes looping through the elements a second time as you've moved back to the first element in the collection instead of moving to the last or stopping when you've reached the last element.

    @dkf said:

    That does not say that the collection that you're modelling needs to pretend that it has no duplicates. It's a sequence of things, that is all you need to know and it's cromulent in itself.

    Nothing in there says you can't have duplicates in the collection (that would be collection specific), but it does spell out that elements are transversed from first to last and then stops.



  • Thanks. Didn't interpret that in that strict sense (don't know why). (Yes, it was a non-rhetorical question.)

    So, we can state, the enumerator effectively defines the collection of which the elements are iterated over.



  • Technically, nothing there prevents you from making a cyclic list, as long as you don't move back to the first element and from the last one. So a 5-element list looping through the middle 3 elements would be perfectly cromulent...


    Filed under: the bad ideas thread, etc.



  • ...but then you'd be violating the MoveNext contract because after element 4, it moves to element 2 instead of the next element.



  • In a cyclic list, 2 is the next element. Nothing stops me from defining an arbitrary order where 2 comes after 4.



  • @Maciejasjmj said:

    In a cyclic list, 2 is the next element. Nothing stops me from defining an arbitrary order where 2 comes after 4.

    Then you get a "Stupid enumeration is stupid" situation. What it boils down to is that foreach iterates through something that is enumerable, if the enumerable thing is broken, don't blame foreach. In other words, foreach doesn't make any guarantee at all - the guarantee is the responsibility of the enumeration itself.



  • @Jaime said:

    Stupid enumeration is stupid

    Duh. But there's nothing violating the contract. And you can always break out of the foreach, so it might even in theory be useful for something that's not hanging your program...



  • @Maciejasjmj said:

    Duh.

    The "x y is x" meme is supposed to invoke "duh", that's the point.

    @Maciejasjmj said:

    so it might even in theory be useful for something that's not hanging your program

    Doubt it.


  • Banned

    @Maciejasjmj said:

    Technically, but that's some hardcore spooky action at a distance when removing an assignment to a variable (or worse - a use of the variable, which would be the case with lambdas) somewhere in the code invalidates the typing of the variable.

    It's not that it can't ever possibly be done, but it really can't be done in a simple way (and doubly so for lambdas with no input types specified), and having to know all the in-depth rules to determine whether your functor has a valid type seems much more of a burden than just using explicit type declarations.


    The rules are simple - if there is any place anywhere in the code where a concrete type is needed, this type will be inferred. If there's nothing to infer from, you must specify it explicitly. If there are conflicts in the inference, it's compilation error.

    I think that the main difference between languages with heavy type inferrence and C# is that in the latter, almost everything is a concrete type due to OOP virus, whereas in e.g. Rust, you mostly use unspecified abstract types you cannot really instantiate. This cleans things up, because 90% of types cannot be the result of type inferrence - while in C#, having a variable of concrete type IEnumerable is perfectly normal, and so is having the same variable with concrete type object.

    It still sucks on C# part.



  • @Jaime said:

    In other words, foreach doesn't make any guarantee at all - the guarantee is the responsibility of the enumeration itself.

    Which is what I said to begin with...



  • @Jaime said:

    Doubt it.

    While not directly useful for a foreach statmenet, infinite sequences are definitely a thing.

    Generating pseudo-random numbers with each MoveNext (You know; like Random.Next already does...) and using the Zip operator to pair them to a sequence is one possible application.

    Reactive programming? Infinite sequences of asynchronously generated events, processed via IEnumerable's pull semantics so you can easily Take, Skip, Aggregate, Join, etc. and create complex workflows. if you can yield return await to block on the next item arriving in the enumerator that could totally work. (Does C# support that? Because that'd be all kinds of awesome as a programming paradigm to try out...)



  • Yes, there's an Rx library from Microsoft for that. Uses IObservable instead of IEnumerable, and seems quite powerful.

    Unless you meant something else?


  • Discourse touched me in a no-no place

    @powerlord said:

    you'd be violating the MoveNext contract

    No, you've just been infected by the “indexing is enumeration” brainworm. There's a thing that has the ability to describe an abstract sequence: that's what the IEnumerable gives you. No guarantee is given that the underlying thing has to actually have that sequence in hand (that would be very undesirable in some cases) or that the sequence has to be a true representation of the thing — that's a promise given by the thing being enumerated. You can also do infinite sequences with the interface, where there's always a next element; as long as you're not expecting the sequence to eventually give a last value, you'll be fine.

    An index is a useful technique for enumerating a List. But not everything is one of those.



  • @Magus said:

    Yes, there's an Rx library from Microsoft for that. Uses IObservable instead of IEnumerable, and seems quite powerful.

    That's exactly what I was gunning for, yes.

    And indeed; it's running on top of IObservable<T> instead of IEnumerable<T>.
    It seems like yield cannot be combined with async / await just yet. It has been proposed a few times though and there is atleast already an IAsyncEnumerable<T> implemented as part of Rx's sister-project Ix (Interactive Extensions). It is even residing in the System namespaces already, instead of the Microsoft namespaces, which is a pretty clear sign that async enumerables are somthing that's expected to occur at some point in time.

    (Uhm... Ok. Apparantly someone has an early prototype of async yield working in a branch of the Roslyn compiler. Nice...)



  • I feel like some of the need is reduced if you're using Rx. I was planning to use it for my game's input, but it freaked out my friend who's working on it with me.

    I've implemented part of LINQ asynchronously before, just for fun. But it's cool seeing what people manage with Roslyn now, too.


Log in to reply