Voltemos no tempo. 2002!
using System; namespace Foo { class Program { static void Main(string[] args) { Point3F p = new Point3F(2, 3, 4); Console.WriteLine("The Point is " + p.ToString()); } } public struct Point3F { public Point3F(float x, float y, float z) { _x = x; _y = y; _z = z; } private readonly float _x; public float X { get { return _x; } } private readonly float _y; public float Y { get { return _y; } } private readonly float _z; public float Z { get { return _z; } } public override string ToString() { return string.Format("{0}; {1}; {2}", _x, _y, _z); } } }
Agora, de volta para o presente. 2019!
using static System.Console; namespace Foo { class Program { static void Main(string[] args) { var p = new Point3<float>(2, 3, 4); WriteLine($"The Point is {p}"); } } public readonly struct Point3<T> { public Point3(T x, T y, T z) => (X, Y, Z) = (x, y, z); public T X { get; } public T Y { get; } public T Z { get; } public override string ToString() => $"{X}, {Y}, {Z}"; } }
Não é difícil perceber, olhando o código do exemplo, que hoje em dia é possível escrever muito menos código, com muito mais expressividade.
As auto-properties
, por exemplo, retiraram a necessidade de criármos campos específicos, manualmente, para armazenar seus valores. Na prática, os campos ainda são criados pelo compilador e, a propóstito, como as propriedades são somente leitura, os campos gerados pelo compilador também são.
Os expression bodied members
também reduziram consideravelmente a quantidade de “{}” espalhadas pelo código e tornaram a leitura do código mais rápida.
A possibilidade de declarar o uso de uma classe estática, com using static
, para acesso direto a seus membros também torna o código mais limpo, embora, um pouco estranho a primeira vista..
As interpolações de string
deram mais fluência para as contatenações.
Por fim, como novidade no C# 8, agora structs
genéricas, quando usadas com parâmetros de tipo sendo tipos primitivos, são valores não gerenciados (unmanaged).
Bônus
A utilização de generics
com structs
, sem prejuizo de performance para parâmetros de tipos primitivos foi um grande avanço. Entretanto, infelizmente, ainda há desafios de generalização que precisam ser superados.
No código que compartilhamos acima, se precisássemos, por exemplo, calcular o “comprimento do vetor”, da origem até o ponto especificado, não teríamos como escrever uma constraint que facilitasse a generalização para numéricos, permitindo o uso simples de funções como Sqrt
. A explicação para isso é dada pelo próprio Anders.
And it’s not clear that the added complexity is worth the small yield that you get. If something you want to do is not directly supported in the constraint system, you can do it with a factory pattern. You could have a Matrix, for example, and in that Matrix you would like to define a dot product method. That of course that means you ultimately need to understand how to multiply two Ts, but you can’t say that as a constraint, at least not if T is int, double, or float. But what you could do is have your Matrix take as an argument a Calculator, and in Calculator, have a method called multiply. You go implement that and you pass it to the Matrix.
Consideramos essa, uma abordagem “inteligente demais”. No mundo real, código “inteligente demais” é difícil de manter.
#nullable enable using System; using System.Collections.Generic; using static System.Console; using static System.Math; namespace Foo { class Program { static void Main(string[] args) { var p = new Point3<float>(3, 4, 0); WriteLine($"The Point is {p}. Length is {p.Length}"); } } public readonly struct Point3<T> { private static readonly ILengthCalculator<T>? LengthCalculator = LengthCalculatorFactory.Of<T>(); public Point3(T x, T y, T z) => (X, Y, Z) = (x, y, z); public T X { get; } public T Y { get; } public T Z { get; } public override string ToString() => $"{X}, {Y}, {Z}"; public double Length => LengthCalculator?.ComputeLength(X, Y, Z) ?? double.NaN; } public interface ILengthCalculator { }; public interface ILengthCalculator<in T> : ILengthCalculator { double ComputeLength(T x, T y, T z); } public class LengthCalculatorForInt : ILengthCalculator<int> { public double ComputeLength(int x, int y, int z) => Sqrt(x * x + y * y + z * z); } public class LengthCalculatorForDouble : ILengthCalculator<double> { public double ComputeLength(double x, double y, double z) => Sqrt(x * x + y * y + z * z); } public class LengthCalculatorForFloat : ILengthCalculator<float> { public double ComputeLength(float x, float y, float z) => Sqrt(x * x + y * y + z * z); } public static class LengthCalculatorFactory { private static readonly Dictionary<Type, ILengthCalculator> Calculators = new Dictionary<Type, ILengthCalculator>() { {typeof(int), new LengthCalculatorForInt()}, {typeof(float), new LengthCalculatorForFloat()}, {typeof(double), new LengthCalculatorForDouble()}, }; public static ILengthCalculator<T>? Of<T>() => (ILengthCalculator<T>) Calculators[typeof(T)]; } }
Se o desejo da Microsoft é adicionar suporte extendido para traits
, por exemplo, a decisão de Anders, no passado, provavelmente terá de ser revista.
De qualquer forma, a inicialização do dicionário e o controle de propagação de nulls
, mostradas no código, também foram grandes avanços da linguagem.