Как улучшить качество Java-кода при использовании нашего транслятора

Поговорим о подходах и языковых конструкциях в C# – какие из них хорошо использовать, а какие нет. Конечно, под хорошим или плохим мы имеем в виду следующее: насколько читаемым и поддерживаемым будет полученный Java-код после перевода из C#.

Маленький в C# – большой в Java

В 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#. С точки зрения производительности, нам следует абстрагироваться от управления памятью, освободив себя от эмуляции этого процесса с двойными затратами.

Связанные статьи