15 марта 2024
Поговорим о подходах и языковых конструкциях в C# – какие из них хорошо использовать, а какие нет. Конечно, под хорошим или плохим мы имеем в виду следующее: насколько читаемым и поддерживаемым будет полученный Java-код после перевода из C#.
В C# есть несколько компактных языковых конструкций, которые выполняют много скрытой работы. Когда вы переводите эти конструкции на другой язык, вам приходится неявно воспроизводить эту скрытую часть работы, и в большинстве случаев код теряет свой первоначальный дизайн и сильно отличается.
Автосвойства очень широко применяются программистами C#. Используя такое свойство, программист может взаимодействовать со скрытым полем посредством методов get и/или set. C# позволяет нам отвлечься от фактической реализации автосвойств и использовать очень компактные конструкции для их объявления. Но в Java такой языковой конструкции нет, и возникает необходимость явно объявлять свойства, как поля так и методы управления доступом:
public int Value { get; set; }
В Java это будет:
private int auto_Value;
public int get_Value()
{
return auto_Value;
}
public void set_Value(int value)
{
auto_Value = value;
}
Теперь это не единый член класса, а три отдельных члена. Представьте себе, что для каждого автосвойства повторяется один и тот же код. Как это будет выглядеть? Желательно избегать подобного. Но как?
Попробуйте заменить автосвойство объектом, который будет обеспечивать доступ к приватному полю. Может существовать хэш-мап таких объектов. Если мы следуем дизайну ограниченного доступа к некоторым данным, это будет хорошим решением. Автосвойства могут выглядеть красиво, но нам не обязательно использовать их без необходимости.
В C# у нас есть специальная логика памяти для структур (типов значений). Их время жизни ограничено временем жизни стекового фрейма или содержащего объекта, и они часто копируются – когда мы передаем их в качестве аргументов функции, возвращаем из функции или присваиваем какому-то полю, мы работаем со значением, а не ссылкой. Изменяя копию, мы не меняем оригинал. Переводя типы значений в Java, мы должны воссоздать ту же логику, хотя классы Java всегда являются ссылочными типами. Частое копирование становится теперь проблемой – чтобы хранить каждую копию, мы выделяем память из кучи, перегружая сборщик мусора. Если мы считаем производительность одним из наших приоритетов, мы должны абстрагировать наш код C# от деталей управления памятью. Но как?
Самый простой способ – сделать типы значений неизменяемыми. Если у вас нет изменяемого состояния, вам нет необходимости копировать это состояние, предотвращая неопределенное поведение.
Самое время поговорить о языковых конструкциях, которые меняют только визуальные свойства кода, но не поведение. Например:
public class Item
{
string name;
string price;
public Item(string name, int price) => (this.name, this.price) = (name, price);
public string ToString() => $"Name = {name}, Price = {price}";
public string Name => name;
public int Price => price;
}
Здесь мы видим деконструкцию кортежа (это выражение на самом деле не создает кортеж), интерполированный строковый литерал, методы и свойства, определенные выражением.
Делегаты удобно использовать, потому что это краткая форма объявления метода. Давайте посмотрим на пример:
using System;
using System.Linq;
class Program
{
delegate int ChangeNumber(int arg);
static void Main()
{
Console.WriteLine("Input some numbers");
int[] numbers = Console.ReadLine().Split(" ").Select(int.Parse).ToArray();
Console.WriteLine("Input addition");
int addition = int.Parse(Console.ReadLine());
ChangeNumbers(n => n + addition, numbers);
Console.WriteLine("Result :");
Console.WriteLine(string.Join(" ", numbers.Select(n => n.ToString())));
}
static void ChangeNumbers(ChangeNumber change, int[] numbers)
{
for(int i = 0; i < numbers.Length; i++)
{
numbers[i] = change(numbers[i]);
}
}
}
Для выражения n => n + addition
мы можем сгенерировать выражение анонимного класса Java:
// translated to Java code
interface ChangeNumber
{
int invoke(int arg);
}
// ...static void main(String[] args)...
// anonymous class expression for Java 7 or older version
changeNumbers(new ChangeNumber()
{
public int invoke(int n)
{
return n + addition;
}
}, numbers);
// or lambda expression for higher Java 8 or newer version
changeNumbers(n -> n + addition, numbers);
В C# имеется множество языковых конструкций, которые могут сделать наш код простым, скрывая большую часть реализации за синтаксическим сахаром. Некоторые из этих конструкций сомнительны и трудно поддерживаемы, другие гибки и легко воспроизводимы. С точки зрения дизайна, лучше составлять абстракции из объектов, а не из специальных языковых конструкций C#. С точки зрения производительности, нам следует абстрагироваться от управления памятью, освободив себя от эмуляции этого процесса с двойными затратами.