Live data from Hacker News

I was wrong, reflecting on the .NET design choices

ayende.com

81–90 of 139 posts

Re: I was wrong, reflecting on the .NET design choices

#81
post #6
post #4

If I attempt to generalize it, I think most of C#'s differences in language design that have opposites in Java are superior. Examples include: + not virtual by default (so base classes can be changed more easily without breaking/recompiling downstream clients that the base class writer doesn't know about; specifying something as "virtual" should be a deliberate conscious decision by the class author) + value types (f…

What do you think of partial classes and methods in term of code quality?

If, like me, you're old enough to have used MFC (where the code generators generated source for classes that you had to augment, and randomly picked lines to replace or overwrite in those classes whenever it felt doing so (1), you think they are a godsend.

(1) that's an exaggeration; MFC used comments to identify sections that it owned in the source, but in my (limited) experience, there typically were zillions of ways to make changes to your code, but if you didn't use the one Microsoft picked as _the_ way (and which they didn't push into your face in the IDE), you were in for heaps of problems.

Re: I was wrong, reflecting on the .NET design choices

#82
post #36

Earlier quoted context omitted.

Neither Swift nor Rust have exceptions, checked or otherwise. The kind of exceptions that unwind the stack until some part of the code up the stack catches the exception. They both handle errors by returning error values, kind of like Go. Rust has a try! macro, which might make you think it's try/catch equivalent, but it's not. It's just a syntactic sugar over error values. Similarly in Swift try/catch/throw is just…

> The kind of exceptions that unwind the stack until some part of the code up the stack catches the exception. Rust panics result in unwinding the stack: https://doc.rust-lang.org/nomicon/unwinding.html try!/? is preferred for handling errors, but if you "know" that a Result or an Option has a value, you can unwrap() or expect() it, and you'll get a panic if you're wrong.

so does panic() in Go. But no-one claims that Go uses exceptions for error handling, because it doesn't and neither does Rust.

In both languages panic() is for "this shouldn't happen" fatal errors, not for signaling errors to the caller, the way Java or C# use exceptions.

My comment was in response to person basically saying "checked exceptions in Java would be good if only they implemented it the way Swift/Rust did".

Re: I was wrong, reflecting on the .NET design choices

#83
post #7

> However, given that I’m working on a database engine now, not on business software, I can see a whole different world of constraints. This might be my own confirmation bias, but this is my takeaway: point of view and the constraints that you see or believe are there are the main determinant of choices, and not whether some particular pattern or feature of a language is intrinsically good. The older I get and the mo…

I have many years of programming in Java under my belt. Until I started using dynamic languages I thought static typing was really important. It's not.

It rules out certain categories of bugs, makes it hard to assign a string to an int, etc...

If you are writing a small one time use script to accomplish a task clearly this that kind of protection is of low value.

If you are trying to write or maintain a system intended to last 20 years and keeps bugs out of 100 millions lines of code, every kind of check that can be automated has extremely high value.

Most projects are somewhere between these two extremes. The nature of the cutoff point where strong static typing helps or does not is what we should be debating, not its inherent value as Dahart suggested.

Re: I was wrong, reflecting on the .NET design choices

#84
post #28

The bigger question is why the hell a post like this makes it on top of hacker news? This was by far the most pointless read ever. On top of that arguing that non-virtual by default is worse than virtual by default is completely superfluous. Just add the damn keyword everywhere and you have virtual everywhere. Same for final. But Java has everything non-final and virtual by default, which sucks badass because both re…

Virtual methods are also significantly slower to invoke.

Significantly?

Have any benchmarks to back that up?

Last time I benchmarked in the language I use most (C++) I couldn't get a difference distinguishable from my margin of error.

Re: I was wrong, reflecting on the .NET design choices

#85
post #49

Earlier quoted context omitted.

As I see it, the Rust (etc) approach avoids two problems: 1. The C problem of forgetting to check error returns. Yes, exceptions (checked or not) also avoid this. 2. The C++/Java/C# problem of exceptions being more expensive than you'd like for common error situations. .NET has sprouted alternatives like `bool tryParse(String, out int)` as a workaround, but on balance I prefer the unified mechanism. What I don't like…

> The C++/Java/C# problem of exceptions being more expensive than you'd like for common error situations. Exceptions should not be used for common error situations! This is the prime mistake of Java which pretty much required exceptions when it should (and checked exceptions to boot). > .NET has sprouted alternatives like `bool tryParse(String, out int)` as a workaround I don't consider that a workaround. There is a…

> There is a deep semantic difference between TryParse and Parse

Sure, but I don't think that splitting every possibly-failing API call into throwing and non-throwing forms is the right way to express that difference, and a lot of the time it'll be a matter of context (meaning that the implementation can't magically do the right thing).

It's fairly easy to layer throwing behaviour on top of nonthrowing in a generic and efficient way (Rust's Option, Java's Optional etc), but the reverse is not true.

I must admit I'm losing track of what if anything we're in disagreement about, though...

Re: I was wrong, reflecting on the .NET design choices

#86
post #80

Earlier quoted context omitted.

Could you explain why garbage collection in your use case is a bad thing? The GC vs non-GC always fascinates me, and I have no strong opinion either way. Also, doesn't C# have the ability to limit (or pretty much disable) GC?

Garbage collection is the bane of smooth framerates. Players notice when the framerate drops. Presuming a pretty typical 60 Frames Per Second you have a tight 16.6ms time budget to do all of the work for the entire frame. All of the physics, all of the sound processing, all of the AI and everything else needs to be sliced up into little bits that can be distributed across the time the game is played. There are many g…

I wonder if the very low-latency GC in Go would be good enough, though? The occasional dropped frame doesn't seem like the end of the world, so long as it remains rare.

In practice, most games don't have entirely reliable performance, particularly on low-end hardware.

Re: I was wrong, reflecting on the .NET design choices

#87
post #85

Earlier quoted context omitted.

> The C++/Java/C# problem of exceptions being more expensive than you'd like for common error situations. Exceptions should not be used for common error situations! This is the prime mistake of Java which pretty much required exceptions when it should (and checked exceptions to boot). > .NET has sprouted alternatives like `bool tryParse(String, out int)` as a workaround I don't consider that a workaround. There is a…

> There is a deep semantic difference between TryParse and Parse Sure, but I don't think that splitting every possibly-failing API call into throwing and non-throwing forms is the right way to express that difference, and a lot of the time it'll be a matter of context (meaning that the implementation can't magically do the right thing). It's fairly easy to layer throwing behaviour on top of nonthrowing in a generic a…

I agree it's easy to layer throwing behavior on top of non-throwing behavior -- Java easily chose the worst possible way to do it.

But having both a Parse and TryParse means that I can ignore the result of the Parse call entirely and let it fall through to the exception handler. It is by-definition always expected to succeed so when it doesn't then that's a bug. If you only have one of TryParse or Parse you cannot judge the intention.

Re: I was wrong, reflecting on the .NET design choices

#88
post #60
post #50

Earlier quoted context omitted.

In addition to the functional stuff, C# has a lot of syntactic sugar that I wouldn't associate with a "boring" language. If you aggressively use all the syntactic sugar from the latest version of C# and compare that to similarly up-to-date Java code, they'll be worlds apart. The C# will look terse, and to more conservative programmers, rather weird.

Not to mention they still have/invent multiple ways to do the same. The old Tuple , anonymous types and new ValueTuples are pretty much different attempts/iterations to achieve a similar goal. I wonder if/how they will unify them or phase some of them out. Btw, i kinda like java's Anonymous Classes. Do we know any reason for C# not to adopt this as well?

> I wonder if/how they will unify them or phase some of them out.

They pretty much never phase anything out because they're serious about backwards compatibility (which frankly I consider refreshing in this world). I mean they've even said at some point like "yeah, the delegates are unfortunate because they're just a more awkward syntax for what the lambda functions do but we're not going to get rid of them because people have used them."

Re: I was wrong, reflecting on the .NET design choices

#89

Earlier quoted context omitted.

When I first learned C#, I was having a hard time to navigate the code due to partial methods. So I have always wondered what C# developers think of having their methods spread out in different files.

Partial classes and partial methods exist specifically to support having code that is automatically generated paired with a human-managed source file. While you could use the support for them to split human-managed code across separate source files, that would be a horrible practice that I've never encountered in the wild, even in .NET shops with otherwise-atrocious practices and code quality.

Partial classes are also useful in the case that you want a nested class to have its own file.

Re: I was wrong, reflecting on the .NET design choices

#90
post #56

Earlier quoted context omitted.

As a low-level game dev, I consider C# a nearly perfect, wonderful language, tragically self-defeated by garbage collection.

Could you explain why garbage collection in your use case is a bad thing? The GC vs non-GC always fascinates me, and I have no strong opinion either way. Also, doesn't C# have the ability to limit (or pretty much disable) GC?

Most problems with GC are when most GC implementations introduce non-deterministic latencies, but it can also be because they demand a large heap size to work well. Even if you can swap the GC implementation or turn it off, can you guarantee that when you turn it on, it will only run for < X milliseconds and then stop/allow you to control it again? If you can't, turning it off only buys you a little, unless your language also supports direct [de]allocation. Nim's strategy with GC with plain access to alloc() is pretty nice.
Post reply on HN