Im Allgemeinen bin ich ein C ++ - Programmierer. Nun, es ist passiert. Die überwiegende Mehrheit des kommerziellen Codes, den ich in meiner Karriere geschrieben habe, ist C ++. Ich mag eine so starke Neigung meiner persönlichen Erfahrung zu einer Sprache nicht wirklich und ich versuche nicht die Gelegenheit zu verpassen, etwas in einer anderen Sprache zu schreiben. Und mein jetziger Arbeitgeber bot plötzlich eine solche Gelegenheit: Ich verpflichtete mich, eines nicht zum trivialsten Dienstprogramm in Java zu machen. Die Wahl der Implementierungssprache wurde aus historischen Gründen getroffen, und es machte mir nichts aus. Java also Java, je weniger vertraut - desto besser.
Unter anderem hatte ich eine ziemlich einfache Aufgabe: einmal einen bestimmten Satz logisch verbundener Daten zu bilden und an einen bestimmten Verbraucher zu übertragen. Es kann mehrere Verbraucher geben, und gemäß dem Kapselungsprinzip hat der Übertragungscode (Hersteller) keine Ahnung, was sich darin befindet und was er mit den Quelldaten tun kann. Der Hersteller benötigt jedoch von jedem Verbraucher die gleichen Daten. Ich wollte keine Kopien machen und sie geben. Dies bedeutet, dass wir den Verbrauchern irgendwie die Möglichkeit nehmen müssen, die an sie übermittelten Daten zu ändern.
Damals machte sich meine Unerfahrenheit in Java bemerkbar. Mir fehlten die Sprachfunktionen im Vergleich zu C ++. Ja, hier gibt es das
final
Schlüsselwort, aber das
final Object
ist wie
Object* const
in C ++, nicht
const Object*
. Das heißt, In der
final List<String>
können Sie beispielsweise Zeilen hinzufügen. Es geht um C ++:
const
überall nach Myers 'Testament zu setzen, und das war's! Niemand wird etwas ändern. Also? Nun, nicht wirklich. Ich habe ein bisschen darüber nachgedacht,
anstatt dieses Dienstprogramm in meiner Freizeit zu machen, und dazu bin ich gekommen.
C ++
Lassen Sie mich Sie an die Aufgabe selbst erinnern:
- Erstellen Sie einmal einen Datensatz.
- Kopieren Sie nichts unnötig.
- Verhindern Sie, dass der Verbraucher diese Daten ändert.
- Code minimieren, d.h. Erstellen Sie nicht für jeden Datensatz, der im Allgemeinen nur an wenigen Stellen benötigt wird, eine Reihe von Methoden und Schnittstellen.
Keine erschwerenden Bedingungen wie Multithreading, Sicherheit im Sinne von Ausnahmen usw. Betrachten Sie den einfachsten Fall. So würde ich es mit der bekanntesten Sprache machen:
foo.hpp #pragma once #include <iostream> #include <list> struct Foo { const int intValue; const std::string strValue; const std::list<int> listValue; Foo(int intValue_, const std::string& strValue_, const std::list<int>& listValue_) : intValue(intValue_) , strValue(strValue_) , listValue(listValue_) {} }; std::ostream& operator<<(std::ostream& out, const Foo& foo) { out << "INT: " << foo.intValue << "\n"; out << "STRING: " << foo.strValue << "\n"; out << "LIST: ["; for (auto it = foo.listValue.cbegin(); it != foo.listValue.cend(); ++it) { out << (it == foo.listValue.cbegin() ? "" : ", ") << *it; } out << "]\n"; return out; }
api.hpp #pragma once #include "foo.hpp" #include <iostream> class Api { public: const Foo& getFoo() const { return currentFoo; } private: const Foo currentFoo = Foo{42, "Fish", {0, 1, 2, 3}}; };
main.cpp #include "api.hpp" #include "foo.hpp" #include <list> namespace { void goodConsumer(const Foo& foo) { // do nothing wrong with foo } } int main() { { const auto& api = Api(); goodConsumer(api.getFoo()); std::cout << "*** After good consumer ***\n"; std::cout << api.getFoo() << std::endl; } }
Natürlich ist hier alles in Ordnung, die Daten bleiben unverändert.
Und wenn jemand versucht, etwas zu ändern?
main.cpp void stupidConsumer(const Foo& foo) { foo.listValue.push_back(100); }
Ja, der Code wird einfach nicht kompiliert.
Fehler src/main.cpp: In function 'void {anonymous}::stupidConsumer(const Foo&)': src/main.cpp:16:36: error: passing 'const std::__cxx11::list<int>' as 'this' argument discards qualifiers [-fpermissive] foo.listValue.push_back(100);
Was könnte schief gehen?
Dies ist C ++ - eine Sprache mit einem reichen Arsenal an Waffen, mit denen Sie auf Ihre eigenen Beine schießen können! Zum Beispiel:
main.cpp void evilConsumer(const Foo& foo) { const_cast<int&>(foo.intValue) = 7; const_cast<std::string&>(foo.strValue) = "James Bond"; }
Ich
const_cast
auch fest, dass die Verwendung von
reinterpret_cast
anstelle von
const_cast
in diesem Fall zu einem Kompilierungsfehler führt. Mit der Besetzung im Stil von C können Sie diesen Fokus jedoch aktivieren.
Ja, ein solcher Code kann zu undefiniertem Verhalten führen
[C ++ 17 10.1.7.1/4] . Er sieht im Allgemeinen misstrauisch aus, was gut ist. Es ist einfacher, während einer Überprüfung zu fangen.
Es ist schlecht, dass sich der Schadcode irgendwo tief im Verbraucher verstecken kann, aber er funktioniert trotzdem:
main.cpp void evilSubConsumer(const std::string& value) { const_cast<std::string&>(value) = "Loki"; } void goodSubConsumer(const std::string& value) { evilSubConsumer(value); } void evilCautiousConsumer(const Foo& foo) { const auto& strValue = foo.strValue; goodSubConsumer(strValue); }
Vor- und Nachteile von C ++ in diesem Zusammenhang
Was ist gut:
- Sie können den Lesezugriff auf alles einfach deklarieren
- Bei der Kompilierung wird ein versehentlicher Verstoß gegen diese Einschränkung festgestellt, weil Konstante und nicht konstante Objekte können unterschiedliche Schnittstellen haben
- Bei einer Codeüberprüfung kann eine bewusste Verletzung festgestellt werden
Was ist schlecht:
- Eine absichtliche Umgehung des Änderungsverbots ist möglich
- und in einer Zeile ausgeführt, d.h. einfach bei der Codeüberprüfung zu überspringen
- und kann zu undefiniertem Verhalten führen
- Die Klassendefinition kann aufgeblasen werden, da unterschiedliche Schnittstellen für konstante und nicht konstante Objekte implementiert werden müssen
Java
In Java wird, wie ich es verstehe, ein etwas anderer Ansatz verwendet. Als
final
deklarierte primitive Typen sind im gleichen Sinne wie in C ++ konstant. Strings in Java sind grundsätzlich unveränderlich, daher benötigen wir in diesem Fall den
final String
.
Sammlungen können in unveränderlichen Wrappern abgelegt werden, für die es statische Methoden der Klasse
java.util.Collections
-
unmodifiableList
unmodifiableMap
,
unmodifiableList
unmodifiableMap
usw. Das heißt, Die Schnittstelle für konstante und nicht konstante Objekte ist dieselbe, aber nicht konstante Objekte lösen beim Versuch, sie zu ändern, eine Ausnahme aus.
Bei benutzerdefinierten Typen muss der Benutzer selbst unveränderliche Wrapper erstellen. Im Allgemeinen ist hier meine Option für Java.
Foo.java package foo; import java.util.Collections; import java.util.List; public final class Foo { public final int intValue; public final String strValue; public final List<Integer> listValue; public Foo(final int intValue, final String strValue, final List<Integer> listValue) { this.intValue = intValue; this.strValue = strValue; this.listValue = Collections.unmodifiableList(listValue); } @Override public String toString() { final StringBuilder sb = new StringBuilder(); sb.append("INT: ").append(intValue).append("\n") .append("STRING: ").append(strValue).append("\n") .append("LIST: ").append(listValue.toString()); return sb.toString(); } }
Api.java package api; import foo.Foo; import java.util.Arrays; public final class Api { private final Foo foo = new Foo(42, "Fish", Arrays.asList(0, 1, 2, 3)); public final Foo getFoo() { return foo; } }
Main.java import api.Api; import foo.Foo; public final class Main { private static void goodConsumer(final Foo foo) {
Fehlgeschlagener Änderungsversuch
Wenn Sie nur versuchen, etwas zu ändern, zum Beispiel:
Main.java private static void stupidConsumer(final Foo foo) { foo.listValue.add(100); }
Dieser Code wird kompiliert, aber zur Laufzeit wird eine Ausnahme ausgelöst:
Ausnahme Exception in thread "main" java.lang.UnsupportedOperationException at java.base/java.util.Collections$UnmodifiableCollection.add(Collections.java:1056) at Main.stupidConsumer(Main.java:15) at Main.main(Main.java:70)
Erfolgreicher Versuch
Und wenn es schlecht geht? Es gibt keine Möglichkeit, das
final
Qualifikationsmerkmal aus dem Typ zu entfernen. Aber in Java gibt es eine viel mächtigere Sache - Reflexion.
Main.java import java.lang.reflect.Field; private static void evilConsumer(final Foo foo) throws Exception { final Field intField = Foo.class.getDeclaredField("intValue"); intField.setAccessible(true); intField.set(foo, 7); final Field strField = Foo.class.getDeclaredField("strValue"); strField.setAccessible(true); strField.set(foo, "James Bond"); }
Ein solcher Code sieht in C ++ noch verdächtiger aus als
cosnt_cast
. Es ist sogar noch einfacher, eine Überprüfung
cosnt_cast
. Und es kann auch zu
unvorhersehbaren Effekten führen (d. H. Hat Java
UB ?). Und es kann sich auch willkürlich tief verstecken.
Diese unvorhersehbaren Auswirkungen können auf die Tatsache zurückzuführen sein, dass der von der
hashCode()
-Methode zurückgegebene Wert möglicherweise gleich bleibt, wenn das
final
Objekt mithilfe von Reflektion geändert wird. Unterschiedliche Objekte mit demselben Hash sind kein Problem, aber identische Objekte mit unterschiedlichen Hashes sind schlecht.
Was ist die Gefahr eines solchen Hacks in Java speziell für Zeichenfolgen (
Beispiel ): Zeichenfolgen können hier im Pool gespeichert werden und sind nicht miteinander verbunden. Dieselben Zeichenfolgen können denselben Wert im Pool anzeigen. Eine geändert - alle geändert.
Aber!
JVM kann mit verschiedenen Sicherheitseinstellungen ausgeführt werden. Der bereits aktivierte
Security Manager
, der aktiviert ist, unterdrückt alle oben genannten Tricks mit Reflexion:
Ausnahme $ java -Djava.security.manager -jar bin/main.jar Exception in thread "main" java.security.AccessControlException: access denied ("java.lang.reflect.ReflectPermission" "suppressAccessChecks") at java.base/java.security.AccessControlContext.checkPermission(AccessControlContext.java:472) at java.base/java.security.AccessController.checkPermission(AccessController.java:895) at java.base/java.lang.SecurityManager.checkPermission(SecurityManager.java:335) at java.base/java.lang.reflect.AccessibleObject.checkPermission(AccessibleObject.java:85) at java.base/java.lang.reflect.Field.setAccessible(Field.java:169) at Main.evilConsumer(Main.java:20) at Main.main(Main.java:71)
Vor- und Nachteile von Java in diesem Zusammenhang
Was ist gut:
- Es gibt ein
final
Schlüsselwort, das die Datenänderung irgendwie einschränkt - Es gibt Bibliotheksmethoden, um Sammlungen unveränderlich zu machen
- Eine bewusste Verletzung der Immunität kann durch Codeüberprüfung leicht erkannt werden
- haben JVM-Sicherheitseinstellungen
Was ist schlecht:
- Ein Versuch, ein unveränderliches Objekt zu ändern, wird nur zur Laufzeit angezeigt
- Um ein Objekt einer bestimmten Klasse unveränderlich zu machen, müssen Sie den entsprechenden Wrapper selbst schreiben
- Ohne entsprechende Sicherheitseinstellungen können unveränderliche Daten geändert werden
- Diese Aktion kann unvorhersehbare Folgen haben (obwohl sie vielleicht gut ist - fast niemand wird das tun)
Python
Nun, danach wurde ich einfach durch die Wellen der Neugier gefegt. Wie werden solche Aufgaben beispielsweise in Python gelöst? Und sind sie überhaupt entschieden? In Python gibt es im Prinzip keine Konstanz, auch wenn es keine solchen Schlüsselwörter gibt.
foo.py. class Foo(): def __init__(self, int_value, str_value, list_value): self.int_value = int_value self.str_value = str_value self.list_value = list_value def __str__(self): return 'INT: ' + str(self.int_value) + '\n' + \ 'STRING: ' + self.str_value + '\n' + \ 'LIST: ' + str(self.list_value)
api.py. from foo import Foo class Api(): def __init__(self): self.__foo = Foo(42, 'Fish', [0, 1, 2, 3]) def get_foo(self): return self.__foo
main.py. from api import Api def good_consumer(foo): pass def evil_consumer(foo): foo.int_value = 7 foo.str_value = 'James Bond' def main(): api = Api() good_consumer(api.get_foo()) print("*** After good consumer ***") print(api.get_foo()) print() api = Api() evil_consumer(api.get_foo()) print("*** After evil consumer ***") print(api.get_foo()) print() if __name__ == '__main__': main()
Das heißt, Es sind keine Tricks erforderlich. Nehmen Sie es und ändern Sie die Felder eines Objekts.
Gentleman's Agreement
Die folgende
Praxis wird in Python akzeptiert:
- Benutzerdefinierte Felder und Methoden, deren Namen mit einem einzelnen Unterstrich beginnen, sind geschützte (in C ++ und Java geschützte ) Felder und Methoden
- Benutzerdefinierte Felder und Methoden mit Namen, die mit zwei Unterstrichen beginnen, sind private Felder und Methoden
Die Sprache macht sogar
Mangeln für "private" Felder. Eine sehr naive Dekoration, kein Vergleich mit C ++, aber dies reicht aus, um unbeabsichtigte (oder naive) Fehler zu ignorieren (aber nicht zu fangen).
Code class Foo(): def __init__(self, int_value): self.__int_value = int_value def int_value(self): return self.__int_value def evil_consumer(foo): foo.__int_value = 7
Und um absichtlich einen Fehler zu machen, fügen Sie einfach ein paar Zeichen hinzu.
Code def evil_consumer(foo): foo._Foo__int_value = 7
Eine weitere Option
Mir hat die von
Oz N Tiram vorgeschlagene Lösung
gefallen . Dies ist ein einfacher Dekorator, der beim Versuch, das
schreibgeschützte Feld zu ändern
, eine Ausnahme auslöst. Dies geht etwas über den vereinbarten Rahmen hinaus („Erstellen Sie keine Reihe von Methoden und Schnittstellen“), aber ich wiederhole, es hat mir gefallen.
foo.py. from read_only_properties import read_only_properties @read_only_properties('int_value', 'str_value', 'list_value') class Foo(): def __init__(self, int_value, str_value, list_value): self.int_value = int_value self.str_value = str_value self.list_value = list_value def __str__(self): return 'INT: ' + str(self.int_value) + '\n' + \ 'STRING: ' + self.str_value + '\n' + \ 'LIST: ' + str(self.list_value)
main.py. def evil_consumer(foo): foo.int_value = 7 foo.str_value = 'James Bond'
Fazit Traceback (most recent call last): File "src/main.py", line 35, in <module> main() File "src/main.py", line 28, in main evil_consumer(api.get_foo()) File "src/main.py", line 9, in evil_consumer foo.int_value = 7 File "/home/Tmp/python/src/read_only_properties.py", line 15, in __setattr__ raise AttributeError("Can't touch {}".format(name)) AttributeError: Can't touch int_value
Dies ist jedoch kein Allheilmittel. Aber zumindest der entsprechende Code sieht verdächtig aus.
main.py. def evil_consumer(foo): foo.__dict__['int_value'] = 7 foo.__dict__['str_value'] = 'James Bond'
Die Vor- und Nachteile von Python in diesem Zusammenhang
Scheint Python sehr schlecht zu sein? Nein, das ist nur eine andere Sprachphilosophie. Normalerweise wird es durch den Satz „
Wir sind alle einwilligende Erwachsene hier “ ausgedrückt (
wir sind alle einwilligende Erwachsene hier ). Das heißt, Es wird davon ausgegangen, dass niemand spezifisch von den akzeptierten Normen abweicht. Das Konzept ist nicht sicher, aber es hat das Recht auf Leben.
Was ist gut:
- Es wird offen erklärt, dass Programmierer Zugriffsrechte überwachen sollten, nicht den Compiler oder Interpreter
- Es gibt eine allgemein anerkannte Namenskonvention für sichere und private Felder und Methoden
- Einige Zugriffsverletzungen können bei einer Codeüberprüfung leicht erkannt werden
Was ist schlecht:
- Auf Sprachebene ist es unmöglich, den Zugriff auf die Felder der Klasse einzuschränken
- Alles beruht ausschließlich auf dem guten Willen und der Ehrlichkeit der Entwickler
- Fehler treten nur zur Laufzeit auf
Geh
Eine andere Sprache, die ich regelmäßig fühle (meistens nur Artikel lesen), obwohl ich noch keine Zeile kommerziellen Codes darauf geschrieben habe. Das
const
Schlüsselwort ist im Grunde vorhanden, aber nur Zeichenfolgen und ganzzahlige Werte, die zur Kompilierungszeit bekannt sind (d. H.
constexpr
aus C ++), können Konstanten sein. Aber die Strukturfelder können nicht. Das heißt, Wenn die Felder als offen deklariert sind, stellt sich heraus, wie in Python - ändern Sie, wen Sie wollen. Nicht interessant. Ich werde nicht einmal einen Beispielcode geben.
Nun, lassen Sie die Felder privat sein und lassen Sie ihre Werte durch Aufrufe offener Methoden erhalten. Kann ich in Go Brennholz bekommen? Natürlich gibt es hier auch Reflexionen.
foo.go package foo import "fmt" type Foo struct { intValue int strValue string listValue []int } func (foo *Foo) IntValue() int { return foo.intValue; } func (foo *Foo) StrValue() string { return foo.strValue; } func (foo *Foo) ListValue() []int { return foo.listValue; } func (foo *Foo) String() string { result := fmt.Sprintf("INT: %d\nSTRING: %s\nLIST: [", foo.intValue, foo.strValue) for i, num := range foo.listValue { if i > 0 { result += ", " } result += fmt.Sprintf("%d", num) } result += "]" return result } func New(i int, s string, l []int) Foo { return Foo{intValue: i, strValue: s, listValue: l} }
api.go package api import "foo" type Api struct { foo foo.Foo } func (api *Api) GetFoo() *foo.Foo { return &api.foo } func New() Api { api := Api{} api.foo = foo.New(42, "Fish", []int{0, 1, 2, 3}) return api }
main.go package main import ( "api" "foo" "fmt" "reflect" "unsafe" ) func goodConsumer(foo *foo.Foo) {
Strings in Go sind übrigens unveränderlich, wie in Java. Slices und Maps sind veränderbar, und im Gegensatz zu Java gibt es im Kern der Sprache keine Möglichkeit, sie unveränderlich zu machen. Nur Codegenerierung (richtig, wenn ich falsch liege). Das heißt, Auch wenn alles richtig gemacht wurde, verwenden Sie keine schmutzigen Tricks, sondern geben Sie das Slice von der Methode zurück - dieses Slice kann jederzeit geändert werden.
Der Gopher-Community
fehlen eindeutig unveränderliche Typen, aber in Go 1.x wird es sicherlich keine geben.
Vor- und Nachteile von Go in diesem Zusammenhang
In meiner unerfahrenen Sicht auf die Möglichkeiten, das Ändern der Felder von Go-Strukturen zu verbieten, liegt es irgendwo zwischen Java und Python, näher an letzterem. Gleichzeitig befolgt Go das Python-Prinzip der Erwachsenen nicht (ich habe es nicht getroffen, obwohl ich es gesucht habe). Aber es gibt: In einem Paket hat alles Zugang zu allem, von den Konstanten bleibt nur ein Rudiment übrig, das Vorhandensein von unveränderlichen Sammlungen. Das heißt, Wenn der Entwickler einige Daten lesen kann, kann er mit hoher Wahrscheinlichkeit dort etwas schreiben. Was wie in Python den größten Teil der Verantwortung vom Compiler auf die Person überträgt.
Was ist gut:
- Alle Zugriffsfehler treten während der Kompilierung auf
- Reflexionsbasierte Dirty Tricks sind im Review deutlich sichtbar
Was ist schlecht:
- Es gibt einfach kein Konzept
- Es ist unmöglich, den Zugriff auf Strukturfelder innerhalb eines Pakets einzuschränken
- Um die Felder vor Änderungen außerhalb des Pakets zu schützen, müssen Sie Getter schreiben
- Alle Referenzsammlungen sind veränderlich
- Mit Hilfe der Reflexion können Sie sogar private Felder ändern
Erlang
Dies ist außer Konkurrenz. Dennoch ist Erlang eine Sprache mit einem ganz anderen Paradigma als die oben genannten vier. Nachdem ich es mit großem Interesse studiert hatte, ließ ich mich sehr gerne in einem funktionalen Stil denken. Leider habe ich keine praktische Anwendung dieser Fähigkeiten gefunden.
In dieser Sprache kann der Wert einer Variablen also nur einmal zugewiesen werden. Und wenn die Funktion aufgerufen wird, werden alle Argumente als Wert übergeben, d. H. Es wird eine Kopie davon erstellt (es gibt jedoch eine Optimierung der Schwanzrekursion).
foo.erl -module(foo). -export([new/3, print/1]). new(IntValue, StrValue, ListValue) -> {foo, IntValue, StrValue, ListValue}. print(Foo) -> case Foo of {foo, IntValue, StrValue, ListValue} -> io:format("INT: ~w~nSTRING: ~s~nLIST: ~w~n", [IntValue, StrValue, ListValue]); _ -> throw({error, "Not a foo term"}) end.
api.erl -module(api). -export([new/0, get_foo/1]). new() -> {api, foo:new(42, "Fish", [0, 1, 2, 3])}. get_foo(Api) -> case Api of {api, Foo} -> Foo; _ -> throw({error, "Not an api term"}) end.
main.erl -module(main). -export([start/0]). start() -> ApiForGoodConsumer = api:new(), good_consumer(api:get_foo(ApiForGoodConsumer)), io:format("*** After good consumer ***~n"), foo:print(api:get_foo(ApiForGoodConsumer)), io:format("~n"), ApiForEvilConsumer = api:new(), evil_consumer(api:get_foo(ApiForEvilConsumer)), io:format("*** After evil consumer ***~n"), foo:print(api:get_foo(ApiForEvilConsumer)), init:stop(). good_consumer(_) -> done. evil_consumer(Foo) -> _ = setelement(1, Foo, 7), _ = setelement(2, Foo, "James Bond").
Natürlich können Sie für jedes Niesen Kopien erstellen und sich so vor Datenkorruption in anderen Sprachen schützen. Aber es gibt eine Sprache (und schon gar keine), in der es einfach nicht anders geht!
Vor- und Nachteile von Erlang in diesem Zusammenhang
Was ist gut:
- Daten können überhaupt nicht geändert werden
Was ist schlecht:
- Kopieren, überall kopieren
Anstelle von Schlussfolgerungen und Schlussfolgerungen
Und was ist das Ergebnis? Abgesehen von der Tatsache, dass ich Staub von ein paar Büchern geblasen habe, die ich vor langer Zeit gelesen habe, habe ich meine Finger ausgestreckt, ein nutzloses Programm in 5 verschiedenen Sprachen geschrieben und die FAQ zerkratzt?
Erstens habe ich aufgehört zu denken, dass C ++ die zuverlässigste Sprache für den Schutz vor einem aktiven Narren ist. Trotz aller Flexibilität und reichhaltigen Syntax. Jetzt neige ich dazu zu glauben, dass Java in dieser Hinsicht mehr Schutz bietet. Dies ist keine sehr originelle Schlussfolgerung, aber für mich selbst finde ich es sehr nützlich.
Zweitens formulierte ich plötzlich für mich die Idee, dass Programmiersprachen grob in solche unterteilt werden können, die versuchen, den Zugriff auf bestimmte Daten auf der Ebene von Syntax und Semantik einzuschränken, und solche, die nicht einmal versuchen, diese Bedenken auf Benutzer zu übertragen . Dementsprechend sollten sich die Einstiegsschwelle, Best Practices und Anforderungen an die Teilnehmer an der Teamentwicklung (sowohl technisch als auch persönlich) je nach ausgewählter Sprache von Interesse unterscheiden. Ich würde gerne zu diesem Thema lesen.
Drittens: Unabhängig davon, wie die Sprache versucht, Daten vor dem Schreiben zu schützen, kann der Benutzer dies auf Wunsch fast immer tun („fast“ wegen Erlang). Und wenn Sie sich auf die gängigen Sprachen beschränken, ist das immer einfach. Und es stellt sich heraus, dass all diese
const
und
final
nichts weiter als Empfehlungen sind, Anweisungen für die korrekte Verwendung von Schnittstellen. Nicht alle Sprachen haben es, aber ich bevorzuge es immer noch, solche Werkzeuge in meinem Arsenal zu haben.
Und viertens das Wichtigste: Da keine (Mainstream-) Sprache einen Entwickler daran hindern kann, böse Dinge zu tun, ist das einzige, was diesen Entwickler auf Trab hält, sein eigener Anstand. Und es stellt sich heraus, dass ich const
meinen Kollegen (und meinem zukünftigen Selbst) nichts verbiete , wenn ich meinen Code eingebe, sondern die Anweisungen überlasse und glaube, dass sie (und ich) ihnen folgen werden. Das heißt,
Ich vertraue meinen Kollegen.Nein, ich weiß seit langem, dass moderne Softwareentwicklung in 99,99% der Fälle Teamarbeit ist. Aber ich hatte Glück, alle meine Kollegen waren „Erwachsene, Verantwortliche“. Für mich war es immer irgendwie so und es ist selbstverständlich, dass alle Teammitglieder die festgelegten Regeln einhalten. Mein Weg zur Erkenntnis, dass wir uns ständig vertrauen und respektieren, war lang, aber verdammt ruhig und sicher.PS
Wenn sich jemand für die verwendeten Codebeispiele interessiert, können Sie diese hier verwenden .