Live data from Hacker News

I was wrong, reflecting on the .NET design choices

ayende.com

31–40 of 139 posts

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

#31

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.

I think I may have manually created partial classes for huge classes to make them more manageable. Think for example expression tree visitors or collections of extension methods. Sure, I could just have created several classes like FooExtensions and BarExtension, but having a partial class Extensions seems ever so slightly better to me. But I generally agree, there are not many good use cases besides code generation and if you are tempted to do it, then you probably have a problem that has a different and better solution.

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

#32
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…

> + no checked exceptions (checked exceptions have benefits in theory but real-world practice shows that it forces programmers to copy-paste mindless boilerplate to satisfy the checked-constraint)

As languages like Rust or Swift demonstrate, the issue is less the checked exceptions and more the abject lack of support for them in the language and type system, which ends up making them essentially unusable.

> Gosling said that unsigned types are confusing and dangerous

He is right of course, but he forgets that so are signed types if they're bounded.

If Java had unbounded signed integers (à la Python or Erlang) that'd be one thing, but Java does not, and neither does it have Pascal-style restrictions/user-defined integral bounds, which means it's no less confusing or dangerous, it's just more limited.

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

#33
While I'm inclined to suspect that non-virtual by default is better from a design perspective‡, don't assume the point about performance is overwhelming. HotSpot has done devirtualization for a long time. You can detect not only when a method is never overridden, but also when it's never overridden at a particular call site. A virtual method that's never overridden can sometimes have no extra overhead, while a virtual method that is overridden may have sufficiently small overhead that it rarely matters.

http://insightfullogic.com/2014/May/12/fast-and-megamorphic-...

‡ I've used non-OO languages, but never an OO language without virtual by default.

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

#34
Java is not "virtual by default", it is virtual-only, except for private methods, which don't participate in inheritence.

I like the Java convention, for one thing, because it is one less decision for programmers to make. I've seen many C# programmers who are oblivious to what virtual means.

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

#35
post #11

One of the greatest lingering flaws in both C# and Java is the lack of metaclasses. Because classes aren't real objects and therefore not necessarily also instances of other classes (their metaclasses) as they would be in Smalltalk, there is no class-side equivalent of "self/this," nor of "super." In effect, you cannot write static (class) methods that call other static methods without explicitly referencing the clas…

> I believe the explosion of factories, abstract factories, and just generally over-engineered object construction and initialization schemes in Java and C# would have been side-stepped if both languages had always had a proper metaclass hierarchy paralleling the regular class hierarchy, as well as some form of local type inference. That's a bit harsh. "Factory" is a term that became prominent in Java as a result of…

First class functions aren't replacements for factories. An abstract factory provides several methods for constructing related objects of different types. The objects come from different type hierarchies, whose inheritance structures mimic each other. For instance, you might have a hierarchy of EncryptionStream and EncryptionKey objects. Both derive in parallel into AESEncryptionStream and AESEncryptionKey. Then you have an EncryptionFactory base class/interface which has MakeStream and MakeKey methods. This is derived into AESEncryptionFactory, whose MakeStream makes an AESEncryptionStream and whose MakeKey makes an AESEncryptionKey.

The client just knows that it has an EncryptionFactory which makes some kind of stream and some kind of key, which are compatible.

AbstractFactory doesn't specifically address indirect construction or indirect use of a class, but it does solve a problem that can also be addressed with metaclasses. If we can just hold a tuple of classes, and ask each one to make an instance, then that kind of makes AbstractFactory go away.

The thing is that in a language like Java, these factories have rigid methods with rigid type signatures. The MakeKey of an EncryptionFactory will typically take the same parameters for all key types. The client doesn't know which kind of stream and key it is using, and uses the factory to make them all in the same way, using the same constructor parameters (which are tailored to the domain through the EncryptionFactory base/interface).

If we have a class as a first class object (such as an instance of a metaclass), that usually goes hand in hand with having a generic construction mechanism. For instance, in Common Lisp, constructor parameters are represented as keyword arguments (a de facto property list). That bootstraps from dynamic typing. All object construction is done with the same generic function in Common Lisp, the generic function make-instance. Thus all constructors effectively have the same type signature.

Without solving the problem of how to nicely have generic constructors, simply adding metaclasses to Java would be pointless. This is possibly a big part of the reason why the feature is absent.

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

#36
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…

> + no checked exceptions (checked exceptions have benefits in theory but real-world practice shows that it forces programmers to copy-paste mindless boilerplate to satisfy the checked-constraint) As languages like Rust or Swift demonstrate, the issue is less the checked exceptions and more the abject lack of support for them in the language and type system, which ends up making them essentially unusable. > Gosling s…

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 syntactic sugar for handling error values.

https://doc.rust-lang.org/book/error-handling.html

https://developer.apple.com/library/content/documentation/Sw...

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

#37

Java is not "virtual by default", it is virtual-only, except for private methods, which don't participate in inheritence. I like the Java convention, for one thing, because it is one less decision for programmers to make. I've seen many C# programmers who are oblivious to what virtual means.

What about "protected final"?

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

#38
post #27

Earlier quoted context omitted.

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.

>While you could use the support for them to split human-managed code across separate source files, that would be a horrible practice To continue that type of advice, some say "#region/#endregion" is another language feature that's intended for code generators so that the IDE can collapse specific lines of code and hide it from view. Programmers should not be hand-coding "#region" themselves. That said, there is deba…

Since it's buried at the bottom of the stackexchange: #region/endregion is useful for visually grouping Unit Tests per method under test. They're also occasionally useful to group interface implementations.

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

#39
post #36

Earlier quoted context omitted.

> + no checked exceptions (checked exceptions have benefits in theory but real-world practice shows that it forces programmers to copy-paste mindless boilerplate to satisfy the checked-constraint) As languages like Rust or Swift demonstrate, the issue is less the checked exceptions and more the abject lack of support for them in the language and type system, which ends up making them essentially unusable. > Gosling s…

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…

> Neither Swift nor Rust have exceptions, checked or otherwise.

The point is that both of them have generic error types which "infect" every caller (transitively) in much the way checked exceptions do.

That Rust and Swift require explicitly bubbling error values should be a point in favour of checked exceptions.

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

#40

While I'm inclined to suspect that non-virtual by default is better from a design perspective‡, don't assume the point about performance is overwhelming. HotSpot has done devirtualization for a long time. You can detect not only when a method is never overridden, but also when it's never overridden at a particular call site. A virtual method that's never overridden can sometimes have no extra overhead, while a virtua…

Since we're talking about performance: the time it takes HotSpot to perform this optimization is also a perf hit for your program.

At the end of the day, the fastest code is one that doesn't have to run.

HotSpot is an impressive technology but the optimizations it has to do to overcome Java's design really only pay for themselves in most frequently executed code paths and only after some time to gather necessary info to perform the optimizations.

It's ok for long-running server code but not good for, say, short-lived command-line program.

Or to put it differently: a language that has perf-friendly design, like Go, matches Java's speed with 10% of engineering time and resources spent on the compiler and optimizations. Perf friendly design means it has to do 10% of the work to achieve the same end result.

Post reply on HN