とりわけ、論理的に関連する特定のデータのセットを作成して特定のコンシューマーに転送するという、かなり単純なタスクがありました。 いくつかのコンシューマーが存在する可能性があり、カプセル化の原則によれば、送信コード(プロデューサー)は、ソースデータで何ができ、ソースデータで何ができるかを知りません。 しかし、製造業者は各消費者が同じデータを受け取る必要があります。 コピーを作ってあげたくありませんでした。 これは、消費者に送信されるデータを変更する機会を何らかの方法で奪う必要があることを意味します。
その時から、Javaでの私の不慣れさが感じられました。 C ++と比較して言語機能が欠けていました。 はい、
final
キーワードがありますが、
final
final Object
は
const Object*
ではなく、C ++の
Object* const
に似ています。 つまり
final List<String>
には、たとえば行を追加できます。 それはC ++ビジネスです。マイヤーズの証言に従って
const
どこにでも配置すること、それだけです! 誰も何も変えません。 だから? まあ、そうでもない。 私は余暇に
C ++
タスク自体を思い出させてください:
- データセットを1回作成します。
- 不必要にコピーしないでください。
- 消費者がこのデータを変更できないようにします。
- コードを最小化、つまり 一般に、ほんの数か所で、必要なデータセットごとに多数のメソッドとインターフェイスを作成しないでください。
マルチスレッド、例外的な意味でのセキュリティなどの悪化する条件はありません。 最も単純なケースを考えてみましょう。 私が最も使い慣れた言語を使用してそれを行う方法は次のとおりです。
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; } }
明らかに、ここではすべてが正常であり、データは変更されていません。
おわりに
*** After good consumer *** INT: 42 STRING: Fish LIST: [0, 1, 2, 3]
そして誰かが何かを変えようとしたら?
main.cpp
void stupidConsumer(const Foo& foo) { foo.listValue.push_back(100); }
はい、コードはコンパイルされません。
エラー
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);
何がおかしいのでしょうか?
これはC ++です-自分の足で撮影するための豊富な武器を備えた言語です! 例:
main.cpp
void evilConsumer(const Foo& foo) { const_cast<int&>(foo.intValue) = 7; const_cast<std::string&>(foo.strValue) = "James Bond"; }
まあ、実際にはすべて:
*** After evil consumer *** INT: 7 STRING: James Bond LIST: [0, 1, 2, 3]
また、この場合に
const_cast
代わりに
const_cast
を使用すると、コンパイルエラーが発生することに注意してください。 ただし、Cスタイルのキャストを使用すると、この焦点を絞ることができます。
はい、そのようなコードは未定義の動作[C ++ 17 10.1.7.1/4]につながる可能性があります。 彼は一般的に疑わしいように見えますが、それは良いことです。 レビュー中にキャッチしやすい。
悪意のあるコードがユーザーの好みの深さまで隠れるのは悪いことですが、それでも動作します:
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); }
おわりに
*** After evil but cautious consumer *** INT: 42 STRING: Loki LIST: [0, 1, 2, 3]
このコンテキストでのC ++の長所と短所
どちらが良いですか:
- 何でも簡単に読み取りアクセスを宣言できます
- この制限の偶発的な違反は、コンパイル段階で検出されます。 定数オブジェクトと非定数オブジェクトは異なるインターフェースを持つことができます
- コードレビューで意識的な違反を検出できる
悪い点:
- 変更の禁止を意識的に回避することが可能です
- 1行で実行されます。 コードレビューでスキップしやすい
- 未定義の動作につながる可能性があります
- 定数オブジェクトと非定数オブジェクトに異なるインターフェースを実装する必要があるため、クラス定義を拡張できます
Java
Javaでは、私が理解しているように、わずかに異なるアプローチが使用されます。
final
として宣言されたプリミティブ型は、C ++と同じ意味で定数です。 Javaの
final String
は基本的に不変であるため、この場合に必要なのは
final String
的な
final String
です。
コレクションは不変のラッパーに配置できます。そのためには、
java.util.Collections
クラスの静的メソッド
unmodifiableList
、
unmodifiableMap
などがあります。 つまり 定数オブジェクトと非定数オブジェクトのインターフェースは同じですが、非定数オブジェクトを変更しようとすると例外がスローされます。
ユーザータイプに関しては、ユーザー自身が不変のラッパーを作成する必要があります。 一般に、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) { // do nothing wrong with foo } public static void main(String[] args) throws Exception { { final Api api = new Api(); goodConsumer(api.getFoo()); System.out.println("*** After good consumer ***"); System.out.println(api.getFoo()); System.out.println(); } } }
おわりに
*** After good consumer *** INT: 42 STRING: Fish LIST: [0, 1, 2, 3]
失敗した変更の試み
たとえば、何かを変更しようとする場合:
Main.java
private static void stupidConsumer(final Foo foo) { foo.listValue.add(100); }
このコードはコンパイルされますが、実行時に例外がスローされます。
例外
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)
成功した試み
そして、悪い方法で? 型から
final
修飾子を削除する方法はありません。 しかし、Javaにはもっと強力なものがあります-リフレクションです。
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"); }
そして免疫に対する
*** After evil consumer *** INT: 7 STRING: James Bond LIST: [0, 1, 2, 3]
このようなコードは、C ++の
cosnt_cast
よりも
cosnt_cast
見えるため、レビューをキャッチするのがさらに簡単です。 そして、それはまた予測不可能な効果につながる可能性があります (つまり、JavaにはUBがありますか?)。 また、任意に深く隠すこともできます。
これらの予測不可能な影響は、リフレクションを使用して
final
オブジェクトが変更されたときに、
hashCode()
メソッドによって返される値が同じままである可能性があるためです。 同じハッシュを持つ異なるオブジェクトは問題になりませんが、異なるハッシュを持つ同じオブジェクトは悪いです。
このようなJavaでの文字列専用のハッキングの危険性は何ですか( 例 )。ここでの文字列はプールに保存でき、互いに無関係で、同じ文字列だけがプール内の同じ値を示すことができます。 変更されたもの-それらすべてを変更しました。
しかし! JVMはさまざまなセキュリティ設定で実行できます。 すでにデフォルトの
Security Manager
がアクティブになっているため、上記のすべてのトリックをリフレクションで抑制します。
例外
$ 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)
このコンテキストでのJavaの長所と短所
どちらが良いですか:
- データの変更を何らかの形で制限する
final
キーワードがあります - コレクションを不変にするライブラリメソッドがあります
- 意識的な免疫違反はコードレビューで簡単に検出されます
- JVMセキュリティ設定がある
悪い点:
- 不変オブジェクトを変更しようとする試みは、実行時にのみ発生します
- 特定のクラスのオブジェクトを不変にするには、適切なラッパーを自分で作成する必要があります
- 適切なセキュリティ設定がない場合、不変データを変更することが可能です
- このアクションは予測不可能な結果をもたらす可能性があります(ただし、これは良いことかもしれません-ほとんど誰もそれを行いません)
Python
まあ、その後、私は単に好奇心の波に流されました。 このようなタスクは、たとえばPythonでどのように解決されますか? そして、彼らはまったく決定されていますか? 実際、Pythonには、そのようなキーワードがなくても、原則として不変はありません。
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()
おわりに
*** After good consumer *** INT: 42 STRING: Fish LIST: [0, 1, 2, 3] *** After evil consumer *** INT: 7 STRING: James Bond LIST: [0, 1, 2, 3]
つまり トリックは必要ありません。それを取り、オブジェクトのフィールドを変更します。
紳士協定
Pythonでは、次のプラクティスが受け入れられます。
- 名前が1つのアンダースコアで始まるカスタムフィールドとメソッドは保護されています(C ++およびJavaで保護されています)フィールドとメソッド
- 2つのアンダースコアで始まる名前のカスタムフィールドとメソッドはプライベートフィールドとメソッドです
この言語は、「プライベート」フィールドのマングリングも行います。 非常に素朴な装飾で、C ++との比較はありませんが、これは意図しない(または素朴な)エラーを無視する(キャッチしない)のに十分です。
コード
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
おわりに
*** After evil consumer *** INT: 42
そして、意図的に間違いを犯すには、いくつかの文字を追加するだけです。
コード
def evil_consumer(foo): foo._Foo__int_value = 7
おわりに
*** After evil consumer *** INT: 7
別のオプション
Oz N Tiramが提案した解決策が気に入りました。 これは単純なデコレータで、 読み取り専用フィールドを変更しようとすると例外がスローされます。 これは合意された範囲を少し超えています(「メソッドとインターフェイスの束を作成しないでください」)が、繰り返しますが、私はそれが好きでした。
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'
おわりに
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
しかし、これは万能薬ではありません。 しかし、少なくとも対応するコードは疑わしいようです。
main.py
def evil_consumer(foo): foo.__dict__['int_value'] = 7 foo.__dict__['str_value'] = 'James Bond'
おわりに
*** After evil consumer *** INT: 7 STRING: James Bond LIST: [0, 1, 2, 3]
このコンテキストでのPythonの長所と短所
Pythonは非常に悪いように見えますか? いいえ、これは言語のもう1つの哲学です。 通常、これは「 ここではすべて同意する大人です 」というフレーズで表されます( ここではすべて同意する大人です )。 つまり 承認された標準から明確に逸脱する者はいないと想定されています。 コンセプトは定かではありませんが、生きる権利があります。
どちらが良いですか:
- プログラマーはコンパイラーまたはインタープリターではなく、アクセス権を監視する必要があると公に宣言されています
- 安全でプライベートなフィールドとメソッドには一般的に受け入れられている命名規則があります
- 一部のアクセス違反はコードレビューで簡単に検出されます
悪い点:
- 言語レベルでは、クラスのフィールドへのアクセスを制限することは不可能です
- すべては開発者の善意と誠実さにのみかかっています
- エラーは実行時にのみ発生します
行く
私が定期的に感じている別の言語(主に記事を読んでいるだけです)ですが、まだ商用コードを書いていません。
const
キーワードは基本的にそこにありますが、コンパイル時に既知の文字列と整数値(つまり、C ++の
constexpr
)のみが定数になります。 しかし、構造フィールドはできません。 つまり フィールドがオープンであると宣言されている場合、Pythonのようになります-希望する人を変更します。 面白くない。 コードの例も挙げません。
さて、フィールドをプライベートにして、値をopenメソッドの呼び出しで取得できるようにします。 Goでfireを入手できますか? もちろん、ここにも反射があります。
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) { // do nothing wrong with foo } func evilConsumer(foo *foo.Foo) { reflectValue := reflect.Indirect(reflect.ValueOf(foo)) member := reflectValue.FieldByName("intValue") intPointer := unsafe.Pointer(member.UnsafeAddr()) realIntPointer := (*int)(intPointer) *realIntPointer = 7 member = reflectValue.FieldByName("strValue") strPointer := unsafe.Pointer(member.UnsafeAddr()) realStrPointer := (*string)(strPointer) *realStrPointer = "James Bond" } func main() { apiInstance := api.New() goodConsumer(apiInstance.GetFoo()) fmt.Println("*** After good consumer ***") fmt.Println(apiInstance.GetFoo().String()) fmt.Println() apiInstance = api.New() evilConsumer(apiInstance.GetFoo()) fmt.Println("*** After evil consumer ***") fmt.Println(apiInstance.GetFoo().String()) }
おわりに
*** After good consumer *** INT: 42 STRING: Fish LIST: [0, 1, 2, 3] *** After evil consumer *** INT: 7 STRING: James Bond LIST: [0, 1, 2, 3]
ところで、Goの文字列はJavaのように不変です。 スライスとマップは可変であり、Javaとは異なり、言語のコアにはそれらを不変にする方法はありません。 コード生成のみ(私が間違っていれば正しい)。 つまり すべてが正しく行われたとしても、ダーティトリックを使用せずに、メソッドからスライスを返すだけです。このスライスはいつでも変更できます。
gopherコミュニティには明らかに不変の型がありませんが、Go 1.xには確かに存在しません。
このコンテキストでのGoの長所と短所
Go構造体のフィールドの変更を禁止する可能性についての私の経験の浅い見解では、JavaとPythonの間のどこかにあり、後者に近いものです。 同時に、GoはPythonの大人の原則を満たしていません(私は探していましたが、会いませんでした)。 ただし、1つのパッケージ内ではすべてがすべてにアクセスでき、定数からは基本的なもののみが残り、変更不可能なコレクションが存在しないことを示します。 つまり 開発者が何らかのデータを読み取れる場合、高い確率で何かを書き込むことができます。 これは、Pythonの場合のように、コンパイラーから人にほとんどの責任を伝えます。
どちらが良いですか:
- すべてのアクセスエラーはコンパイル中に発生します
- 反射ベースのダーティトリックはレビューではっきりと見える
悪い点:
- 「読み取り専用データセット」の概念は単純ではありません
- パッケージ内の構造フィールドへのアクセスを制限することは不可能です
- パッケージ外の変更からフィールドを保護するには、ゲッターを作成する必要があります
- すべての参照コレクションは変更可能です
- リフレクションの助けを借りて、プライベートフィールドを変更することもできます
アーラン
これは競争の対象外です。 それでも、Erlangは上記の4つとは非常に異なるパラダイムを持つ言語です。 興味を持って勉強したら、自分を機能的なスタイルで考えさせるのが本当に好きでした。 しかし、残念ながら、これらのスキルの実用的な応用は見つかりませんでした。
そのため、この言語では、変数の値は一度しか割り当てることができません。 そして、関数が呼び出されると、すべての引数は値で渡されます。 それらのコピーが作成されます(ただし、末尾再帰の最適化があります)。
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").
おわりに
*** After good consumer *** INT: 42 STRING: Fish LIST: [0,1,2,3] *** After evil consumer *** INT: 42 STRING: Fish LIST: [0,1,2,3]
もちろん、くしゃみごとにコピーを作成できるため、他の言語のデータ破損から身を守ることができます。 しかし、他の方法でそれを単純に行うことのできない言語があります(確かにそうではありません)。
このコンテキストでのErlangの長所と短所
どちらが良いですか:
- データはまったく変更できません
悪い点:
- コピー、どこでもコピー
結論と結論の代わりに
そして、結果は何ですか? まあ、昔読んだ本からほこりを吹き飛ばしたという事実に加えて、私は指を伸ばし、5つの異なる言語で役に立たないプログラムを書き、FACを傷つけましたか?
まず、C ++がアクティブなバカに対する保護の観点から最も信頼できる言語であると考えるのをやめました。 すべての柔軟性と豊富な構文にもかかわらず。 今、私はこの点でJavaがより多くの保護を提供すると考える傾向があります。 これは非常に独創的な結論ではありませんが、私にとっては非常に便利です。
第二に、プログラミング言語は、構文とセマンティクスのレベルで特定のデータへのアクセスを制限しようとする言語と、ユーザーにこれらの懸念を移そうとしない言語とに大まかに分けることができるという考えを突然思いつきました。 。 したがって、エントリのしきい値、ベストプラクティス、チーム開発参加者(技術的および個人的)の要件は、選択した関心のある言語によって何らかの形で異なる必要があります。 このテーマについて読みたいです。
3番目:言語がどのようにデータを書き込みから保護しようとしても、ユーザーは必要に応じてほぼいつでもこれを行うことができます(Erlangのおかげで「ほぼ」)。 そして、あなたが主流の言語にとらわれているなら、それはいつも簡単です。 そして、これらの
const
と
final
はすべて、推奨事項、つまりインターフェースを正しく使用するための指示にすぎません。 すべての言語がそれを持っているわけではありませんが、私はまだそのようなツールを自分の武器に持つことを好みます。
そして第4に、最も重要なことです。(主流の)言語は開発者が厄介なことを行うことを妨げることができないため、この開発者を維持する唯一のことは彼自身の良識です。そして
const
、コードを入れるとき、同僚(および私の将来の自分)に何かを禁止するのではなく、彼ら(そして私)が彼らに従うと信じて指示を残していることがわかります。つまり私は同僚を信頼しています。
いいえ、私は長い間、最新のソフトウェア開発がチーム作業の99.99%であることを知っています。しかし、私は幸運でした、私の同僚はすべて「大人、責任者」でした。私にとっては、常にそうであり、すべてのチームメンバーが確立されたルールを順守することは当然のことです。私たちが絶えずお互いを信頼し尊敬しているという認識に至るまでの道のりは長いものでしたが、冷静で安全です。
PS
誰かが使用されたコード例に興味があるなら、あなたはそれらをここで取ることができます。