15 März 2024

Wie man die Java-Codequalität verbessert, wenn man unseren Übersetzer verwendet

Wir werden über Ansätze und Sprachkonstrukte in C# sprechen: welche gut zu verwenden sind und welche nicht gut sind. Natürlich betrachten wir unter “gut” oder “nicht gut” Folgendes: Wie lesbar und wartbar wird der resultierende Java-Code nach der Übersetzung aus C# sein?

Kompakt in C# –- umfangreich in Java

C# verfügt über einige kompakte Sprachkonstrukte, die viel versteckte Arbeit verrichten. Wenn Sie diese Konstrukte in eine andere Sprache übersetzen, müssen Sie diesen versteckten Teil implizit reproduzieren, und in den meisten Fällen verliert der Code dadurch sein ursprüngliches Design und unterscheidet sich stark.

Auto-Eigenschaften

Auto-Eigenschaften werden von C#-Programmierern weit verbreitet verwendet. Mit dieser Art von Eigenschaft kann der Programmierer über die get- und/oder set-Methode auf das versteckte Feld zugreifen. C# erlaubt es uns, uns von der tatsächlichen Implementierung der Auto-Eigenschaften abzulenken und sehr kompakte Konstrukte zu ihrer Deklaration zu verwenden. In Java haben wir jedoch keine solche Sprachkonstruktion, und es ist notwendig, Eigenschaften explizit zu deklarieren: als Feld und Zugriffskontrollmethoden:

public int Value { get; set; }

In Java wird es zu:

private int auto_Value;
public int get_Value()
{
    return auto_Value;
}
public void set_Value(int value)
{
    auto_Value = value;
}

Jetzt handelt es sich nicht mehr um ein einheitliches Element, sondern um drei separate Elemente. Stellen Sie sich vor, der gleiche Code würde für jede Auto-Eigenschaft wiederholt – wie würde das aussehen? Lassen Sie uns solche unangenehmen Erfahrungen vermeiden. Aber wie?
Versuchen Sie, die Auto-Eigenschaft durch ein Objekt zu ersetzen, das Zugriffskontrolle über das private Feld bietet. Es könnte eine Hashmap solcher Objekte geben. Wenn wir ein Design mit begrenztem Zugriff auf bestimmte Daten verfolgen, wäre dies ein guter Weg. Auto-Eigenschaften mögen nett aussehen, aber wir müssen sie nicht ohne Notwendigkeit verwenden.

Wertetypen

In C# haben wir eine spezielle Speicherlogik für Strukturen (Wertetypen). Ihre Lebensdauer ist auf die Lebensdauer des Stackrahmens oder des enthaltenen Objekts beschränkt, und sie werden häufig kopiert -– wenn wir sie als Funktionsargumente übergeben, aus einer Funktion zurückkehren oder einem Feld zuweisen, arbeiten wir mit dem Wert, nicht mit der Referenz. Wenn wir eine Kopie ändern, ändern wir nicht das Original. Bei der Übersetzung von Wertetypen nach Java müssen wir dieselbe Logik neu erstellen, obwohl Java-Klassen immer Referenztypen sind. Häufiges Kopieren wird jetzt zum Problem – um jede Kopie zu speichern, allozieren wir Speicher aus dem Heap und überlasten den Garbage Collector. Wenn wir die Leistung als eines unserer Interessen betrachten, müssen wir unseren C#-Code von Details zur Speicherverwaltung abstrahieren. Aber wie?
Der einfachste Weg, dies zu tun, besteht darin, Ihre Wertetypen unveränderlich zu machen. Wenn Sie keinen veränderlichen Zustand haben, müssen Sie diesen Zustand nicht kopieren, um unbestimmtes Verhalten zu verhindern.

Nur-Syntax-Konstrukte

Jetzt ist es an der Zeit, über Sprachkonstrukte zu sprechen, die nur die visuellen Eigenschaften des Codes ändern, aber nicht das Verhalten. Zum Beispiel:

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;
}

Hier sehen wir die Zerlegung eines Tupels (dieser Ausdruck erstellt tatsächlich kein Tupel), einen interpolierten Zeichenkettenliteral sowie ausdrucksbasierte Methoden und Eigenschaften.

Delegaten

Delegaten sind gut zu verwenden, weil sie eine verkürzte Form der Methodendeklaration darstellen. Schauen wir uns das Beispiel an:

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]);
	    }
	}
}

Für den Ausdruck n => n + addition können wir in Java einen anonymen Klassen-Ausdruck generieren:

// 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);

Fazit

C# bietet viele Sprachkonstrukte, die unseren Code einfach aussehen lassen und eine große Implementierung hinter Zuckersyntax verbergen können. Einige dieser Konstrukte sind fragwürdig und schwer unterstützbar, andere sind flexibel und leicht reproduzierbar. Wenn es um Design geht, ist es besser, Abstraktionen durch Objekte zu komponieren, nicht durch C#-spezifische Sprachkonstrukte. Wenn es um Leistung geht, sollten wir uns von der Speicherverwaltung abstrahieren und uns von der doppelten Kostenemulation befreien.

Verwandte Nachrichten

In Verbindung stehende Artikel