Open ZacharyPatten opened 5 years ago
Thanks for reporting @ZacharyPatten! As you described in the email, you are not interested in pursuing these improvements in UnitsNet yourself, just pointing out some source of reference we can look at when we address these two points.
There are a number of related issues on this already:
struct
vs class
struct
to class
decimal
based nugetBelow is my reply in the email for future reference.
To address your points:
Yup, this is a pain point for some users. We are currently restricted by using
struct
instead ofclass
, which is by design in order to get value vs reference semantics (no null checking, pass by value, more similar to .NET types like DateTime and TimeSpan). We do however have some open discussions on this topic already and although I have probably been the biggest opponent to changing the semantics without carefully evaluating the pros/cons first, I do see that there are considerable wins by moving toclass
; generics, inheritance, a lot less duplicated code and smaller binary size.Performance has not been a priority in the past, but I am always open to ideas. We don't do any caching so that sounds fairly straight forward to add. We also recently refactored the conversions from hard coded switches (in generated code) to dynamic lookup tables. It adds another method invocation in the conversion, not sure about that perf impact, but it gives us a constant lookup time and the possibility of adding direct conversion functions from say meter to centimeter - and maybe even automating conversions between prefixes (centi- to kilo-). It also opens up the option for plugging in third party conversions by the consumers. I guess we'll just have to profile it and see how well it performs, but again, not a big priority for now.
So yeah, absolutely valid points, but I don't have the time or enough personal interest in these particular changes to perform them myself anytime soon, but if you or someone else wants to take it on I'm happy to assist on any pull requests!
Thanks for the project link, it will be useful to see how you tackled similar things if we pursue generics and performance later.
In regards to #285 I see that MathNet was mentioned as an example of generic mathematics. Do not copy MathNet's pattern. It is not scaleable. You shouldn't use abstract methods or other forms of inheritance. You should use runtime compilation.
My Towel project is using runtime compilation in order to perform generic mathematics. This is a much better pattern because you don't have to write a custom implementation for every type you want to support.
I wrote an old blog post about this topic if anyone is interested. However I wrote the blog post before I knew about Linq expressions!!!! Linq expressions (as I'm using in the Towel project) allow for very clean runtime compilation code. But you may still find the blog post interesting: http://towelcode.com/old-c-generic-math-article-12-may-2015/
Hope that helps.
Thanks for your insights, I just read the blog post now and you have definitely ventured into some terrain that I haven't before. Interesting stuff about compiling at runtime.
We have used c# code generation (pre-compile, not at runtime) extensively in this project and that would be one option for us as well, but it does bloat the binary size quite a bit so if we can instead reuse code for the the 3 main numeric types (float, double, decimal) that will probably save us a bunch.
I believe we did discover some options for arithmetic with generics at some point - I just don't remember the details from the top of my head. @tmilnthorp had some ideas I believe.
Will definitely check out your linq expressions and runtime compilation for inspiration :thumbsup:
Hey. I just wanted to mention that I got prototypes of "Speed" and "Acceleration" working with generics working in Towel if you want to take a look. This is significant because those are not base measurement types (they are complex/derived measurements).
It required a different pattern than UnitsNet currently uses. UnitsNet only has one enum value per quantity, but the prototypes in Towel use potentially multiple enum values per quantity/measurement.
Towel Speed Construction Example:
Speed<double> a = new Speed<double>(1.5d, Meters / Seconds);
Speed<double> b = new Speed<double>(2.5d, Knots); // converted to length & time units under the hood
Towel Acceleration Construction Example:
Acceleration<double> c = new Acceleration<double>(3.5d, Meters / Seconds / Seconds);
The reason I found this was necessary is because if you want to support non-rational types (example: int), then you always need to auto-convert from larger unit to smaller unit so there is minimal rounding (won't round to zero for int).
I did some very primitive speed testing and my prototypes appear to still be faster (for speed and acceleration) than UnitsNet currently is regardless of the extra enum allocations.
Still a lot of testing/tinkering to do, but it is working so far. :)
If you don't care about supporting integer types, you probably don't need to store multiple enum values per quantity/measurement, but I wanted to support "int" as much as possible in Towel.
Hope that makes sense. Just trying to help.
Thanks a lot for chiming in @ZacharyPatten , I'm super busy with work these days and have a hard time following up, but it is much appreciated that you drop knowledge and experiences for us to learn from! I don't think we will ever support the "int" scenario you mention, but interesting to learn how you approached it. Multiple enum values is interesting, I'd have to think some more on it to comment anything meaningful on it though :-)
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.
Pinning this issue, at some point we need to address this.
Would you be amenable to a major release soon with only this?
Absolutely, if we can get generics working so we can get rid of the mix of double + decimal as well as allow the consumer to choose numeric type (float, double, decimal) then I'm game for a breaking change.
Note also that #651 might be getting some traction too, which would be perfect to include in a major bump.
I don't recall, does this mean we have to go to class
or can we still keep struct
?
The generics will work with either classes or structs. That is a seperate topic of which is better classes or structs? I was using "struct" in my code, but I have yet to do some performance testing.
I was going to leave it as struct
Awesome, then I don't see any reason not to go forward with this.
We can't merge this into master
branch until all breaking changes are ready, so I just created release/v5
branch for this purpose. Point any breaking change PRs there.
A concept of a very generic units of measure library that may help you somehow: github.
Interesting use of the new Math API @WhiteBlackGoose 👍 We have been contemplating how we can best utilize it for UnitsNet. I will keep this reference in mind!
Generic Types
Right now, UnitsNet can only support "double" types. If you use generic types on the quantities you can allow for support of other types than just double.
I have working prototypes for Length, Mass, and Angle measurement types using generic paramters here: https://github.com/ZacharyPatten/Towel
I wanted to share my patterns with UnitsNet, because they could help improve UnitsNet should they be adopted. However, there are pros/cons to my pattern, and it would be a massive overhaul of UnitsNet to incorporate generics, so it might not be feasible for the project to adopt the patterns.
Performance Optimization
The conversions in UnitsNet require a double conversion to/from a base unit. In that Towel project, I am using multiplication tables (in the form of jagged arrays) to cache the conversions between units. The index of the multiplication tables are provided by the values on the unit enums. This allows Towel to perform unit conversions with a single operation rather than double as UnitsNet is currently doing.