Вообще я C++ программист. Ну так получилось. Подавляющее большинство коммерческого кода, который я написал за свою карьеру, — это именно C++. Мне не очень нравится такой сильный перекос моего личного опыта в сторону одного языка, и я стараюсь не упускать возможности написать что-нибудь на другом языке. И мой текущий работодатель внезапно такую возможность предоставил: я взялся сделать одну не самую тривиальную утилиту на Java. Выбор языка реализации был сделан по историческим причинам, да я и не возражал. Java так Java, чем менее мне знакомо — тем лучше.

Помимо прочего возникла у меня довольно простая задача: единожды сформировать некий набор логически связанных данных и передать его некоему потребителю. Потребителей может быть несколько, и согласно принципу инкапсуляции передающий код (производитель) понятия не имеет, что там внутри и что оно может сделать с исходными данными. Но производителю надо, чтобы каждый потребитель получил одни и те же данные. Делать копии и отдавать их мне не хотелось. Значит, надо как-то лишить потребителей возможности изменять переданные им данные.

Тут-то моя неопытность в Java и дала о себе знать. Мне не хватало возможностей языка по сравнению с C++. Да, тут есть ключевое слово final, но final Object — это как Object* const в C++, а не const Object*. Т.е. в final List<String> можно добавлять строки, например. То ли дело C++: понавставлять везде const по заветам Майерса, и все! Никто ничего не изменит. Так? Ну не совсем. Я немного поразмышлял на эту тему вместо того, чтобы делать ту утилиту на досуге, и вот к чему я пришел.

С++


Напомню саму задачу:

  1. Единожды создать набор данных.
  2. Ничего не копировать без надобности.
  3. Запретить потребителю менять эти данные.
  4. Минимизировать код, т.е. не создавать кучу методов и интерфейсов для каждого набора данных, нужного в общем-то всего в паре мест.

Никаких отягчающих условий типа многопоточности, безопасности в смысле исключений и т.п. Рассмотрим самый простой случай. Вот как бы я это сделал с использованием наиболее знакомого мне языка:

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]


Замечу еще, что использование reinterpret_cast вместо const_cast в данном случае приведет к ошибке компиляции. А вот приведение в стиле C позволит провернуть этот фокус.

Да, такой код может привести к Undefined Behavior [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++ в данном контексте


Что хорошо:
  • можно легко объявить доступ на чтение к чему угодно
  • случайное нарушение этого ограничения выявляется на этапе компиляции, т.к. константные и неконстантные объекты могут иметь разные интерфейсы
  • сознательное нарушение может быть выявлено на код-ревью

Что плохо:
  • сознательный обход запрета на изменения возможен
  • и выполняется в одну строчку, т.е. его легко пропустить на код-ревью
  • и может привести к неопределенному поведению
  • определение класса может раздуваться из-за необходимости реализации разных интерфейсов для константных и не константных объектов


Java


В Java, как я понял, используется несколько другой подход. Примитивные типы, объявленные как final, — являются константными в том же смысле, что и в C++. Строки в Java в принципе неизменяемы, так что final String — то, что надо в данном случае.

Коллекции же можно поместить в неизменяемые обертки, для чего есть статические методы класса java.util.CollectionsunmodifiableList, 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]


Такой код выглядит еще более подозрительно, чем cosnt_cast в C++, его еще проще отловить на ревью. И он тоже может привести к непредсказуемым эффектам (т.е. в 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’е? И решаются ли вообще? Ведь в питоне никакой константности нет в принципе, даже ключевых слов таких нет.

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]


Т.е. никаких ухищрений просто не надо, бери да меняй поля любого объекта.

Джентльменское соглашение


В питоне принята следующая практика:
  • пользовательские поля и методы, чьи имена начинаются с одного подчеркивания, — это защищенные (protected в C++ и Java) поля и методы
  • пользовательские поля и методы с именами, начинающимися с двух подчеркивания, — это приватные (private) поля и методы

Язык даже делает декорацию (mangling) для «приватных» полей. Весьма наивную декорацию, никакого сравнения с 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. Это простой декоратор, который при попытке изменить read only поле кидает исключение. Это немного выходит за оговоренные рамки («не создавать кучу методов и интерфейсов»), но, повторюсь, мне понравилось.

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 в данном контексте


Кажется, что в питоне все очень плохо? Нет, это просто другая философия языка. Обычно она выражается фразой «Мы все тут взрослые, ответственные люди» (We are all consenting adults here). Т.е. предполагается, что никто специально не будет отклоняться от принятых норм. Концепция не бесспорная, но право на жизнь имеет.

Что хорошо:
  • открыто декларируется, что за правами доступа должны следить программисты, а не компилятор или интерпретатор
  • есть общепринятое соглашение об именах защищенных и приватных полей и методов
  • некоторые нарушения прав доступа легко выявляются на код-ревью

Что плохо:
  • на уровне языка невозможно ограничить доступ к полям класса
  • все держится исключительно на доброй воле и честности разработчиков
  • ошибки проявляются только во время выполнения


Go


Еще один язык, который я периодически щупаю (в основном просто читаю статьи), хотя пока не написал на нем ни строчки коммерческого кода. Ключевое слово const тут в принципе есть, но константами могут быть только строки и целочисленные значения, известные во время компиляции (т.е. constexpr из C++). А поля структуры — не могут. Т.е. если поля объявлены открытыми, то получается как в питоне — меняй, кто хочешь. Неинтересно. Даже пример кода приводить не буду.

Ну ладно, пусть поля будут приватными, и пусть их значения можно получить через вызовы отрытых методов. Получится ли наломать дров в Go? Конечно, тут ведь тоже есть рефлексия.

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 в ядре языка нет способа сделать их неизменяемыми. Только кодогенерация (поправьте, если я ошибаюсь). Т.е. даже если все сделать правильно, не использовать грязных трюков, просто возвращать слайс из метода — этот слайс всегда можно изменить.

Сообществу гоферов явно не хватает неизменяемых типов, но в Go 1.x их точно не будет.

Преимущества и недостатки Go в данном контексте


На мой неискушенный взгляд по возможностям запрета менять поля структур Go находится где-то между Java и Python, ближе к последнему. При этом в Go нет (я не встречал, хотя искал) питоновского принципа про взрослых людей. Но есть: внутри одного пакета все имеет доступ ко всему, от констант остался только рудимент, наличие отсутствия неизменяемых коллекций. Т.е. если разработчик может какие-то данные считать, то с большой вероятностью он может чего-то туда и записать. Что, как и в питоне, передает большую часть ответственности от компилятора к человеку.

Что хорошо:
  • все ошибки доступа проявляются во время компиляции
  • грязные трюки на основе рефлексии хорошо заметны на ревью

Что плохо:
  • понятия «набор данных только для чтения» просто нет
  • невозможно ограничить доступ к полям структуры в пределах пакета
  • чтобы защитить поля от изменений за пределами пакета, придется писать геттеры
  • все ссылочные коллекции изменяемы
  • с помощью рефлексии можно изменять даже приватные поля


Erlang


Это вне конкурса. Все-таки Erlang — язык с очень отличной от вышеупомянутых четырех парадигмой. Когда-то я изучал его с большим интересом, мне очень нравилось заставлять себя мыслить в функциональном стиле. Но практического применения этим навыкам я, к сожалению, не нашел.

Так вот, в этом языке значение переменной можно присвоить только один раз. И при вызове функции все аргументы передаются по значению, т.е. делается их копия (зато есть оптимизация хвостовой рекурсии).

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 разных языках, и почесал ЧСВ?

Во-первых, я перестал считать, что C++ — самый надежный в плане защиты от активного дурака язык. Несмотря на все его гибкость и обильный синтаксис. Сейчас я склоняюсь к мысли, что Java в этом плане дает больше защиты. Это не очень оригинальный вывод, но для себя я нахожу его весьма полезным.

Во-вторых, я вдруг сформулировал для себя мысль, что очень грубо языки программирования можно разделить на те, которые пытаются на уровне синтаксиса и семантики ограничить доступ к тем или иным данным, и на те, которые даже не пытаются, перекладывая эти заботы на пользователей. Соответственно, порог вхождения, лучшие практики, требования к участникам командной разработки (как технические, так и личностные) — должны как-то отличаться в зависимости от выбранного ЯП. С удовольствием почитал бы на эту тему.

В-третьих: как бы язык ни пытался защитить данные от записи, при желании пользователь почти всегда может это сделать («почти» из-за Erlang’а). А если ограничиться мейнстримовыми языками — то просто всегда. И получается, что все эти const и final — не более чем, рекомендации, инструкции по правильному использованию интерфейсов. Не во всех языках это есть, но я все-таки предпочитаю иметь в своем арсенале такие средства.

И в-четвертых, самое главное: раз уж никакой (мейнстримовый) язык не может запретить разработчику сделать гадость, единственное, что этого разработчика удерживает — это собственная порядочность. И получается, что я, расставляя const в своем коде, не запрещаю что-то своим коллегам (и будущему себе), а оставляю инструкции, полагая, что они (и я) будут им следовать. Т.е. я доверяю своим коллегам.

Нет, я давно знаю, что современная разработка ПО — это в 99.99% случаев командная работа. Но мне везло, все мои коллеги были «взрослыми, ответственными» людьми. Для меня всегда как-то и было, и есть само собой разумеющимся, что все члены команды соблюдают установленные правила. Мой путь к осознанию того, что мы постоянно доверяем и уважаем друг друга был долгим, но, черт возьми, спокойным и безопасным.

P. S.


Если кому-то интересны использованные примеры кода, их можно взять здесь.

Комментарии (11)


  1. knstqq
    12.04.2019 10:10

    Обычно, интерфейс класса — это набор «соглашений» о том, какие данные и как будут переданы, какой функционал существует.
    Для безопасности в смысле БЕЗОПАСНОСТИ никогда не следует шарить данные для недоверенных потребителей или надеяться на const. Нужно использовать средства операционных систем для общения с недоверенными модулями или системами, используя каналы/шаренную память/read-only страницы памяти и все остальные крутые штуки, которые предоставляются ядром и интерфейсом операционных систем, либо специальными средствами защиты, такие как SELinux/AppArmor/etc


    1. Fyret Автор
      12.04.2019 10:51

      Это для совсем уже «чужих». В рамках одной кодовой базы остается только const.


  1. gibson_dev
    12.04.2019 10:24

    в erlang данные под капотом на самом деле не копируются без надобности (copy on write), даже если вы изменяете только часть данных — то только они и изменятся на остальные будут ссылки. Поэтому расход памяти не особо и большой получается.


  1. phantom-code
    12.04.2019 11:19

    Вопрос преимуществ/недостатков константности на самом деле весьма непрост. После того, как я столкнулся с невозможностью использовать const correctness в языке D, как я это делал в C++, мне пришлось пройти через путь «отрицание/гнев/принятие» и пересмотреть свои взгляды на эту тему. Есть весьма интересная статья «Why const sucks», написанная одним из котрибьюторов языка D. Его размышления заставляют задуматься, а действительно ли так необходима логическая константность в C++, как я думал раньше?


  1. evgenyk
    12.04.2019 11:44

    А я так и не понимаю, зачем защищать от разработчика? ИМХО, наилучший вариант это питоновский, в нем:
    — все написано явно и честно;
    — в сумме порождает наименьшее количество геморроя.


    1. Fyret Автор
      12.04.2019 11:58

      Вопрос привычки, полагаю. Мне после С++ постоянно не хватает чего-то в других языках. Кому-то после Python в С++ будет казаться, что язык предлагает/требует слишком много.


  1. 0xd34df00d
    12.04.2019 21:42

    Можно пойти ещё немножко дальше и вместо эрланга рассмотреть хаскель. Там и поменять не получится, и при передаче значения в функцию ничего не копируется.


  1. technic93
    12.04.2019 23:24

    Было бы интересно в сравнение добавить Rust, там всё по умолчанию immutable, а для того чтобы сделать что-то не const (в смысле плюсов) надо прописовать ключевое слово mut.
    На счёт средств обхода этого ограничения не знаю, но наверняка можно внутри unsafe блока.


    1. Fyret Автор
      15.04.2019 09:33

      Эх, несколько лет назад я выбирал как раз между Go и Rust, но выбрал Go. Так что с Rust я не знаком. Но мне тоже было бы интересно :)


  1. Maccimo
    15.04.2019 01:34

    Чем еще опасен такой хак в Java: строки в здесь могут храниться в пуле, и на одно и то же значение в пуле могут указывать никак не связанные друг с другом, просто одинаковые строки. Изменил одну — изменил их все.

    Вы не изменяли строку, вы изменили ссылку на экземпляр строки.
    До вызова evilConsumer() поле strValue содержало ссылку на экземпляр класса String со значением "Fish", а после — на другой экземпляр, хранящий значение "James Bond".
    Рыба при этом не пострадала и всё так же оставалась рыбой.


    Для того, чтобы превратить рыбу в Джеймса Бонда вам пришлось бы залезть через reflection в экземпляр строки. Делать так, естественно, не нужно.


    1. Fyret Автор
      15.04.2019 09:31

      Неудачно получилось. Я изменил строку (код), но убрал этот пример из текста, чтобы не перегружать статью.
      Спасибо за замечание. Подумаю, как переписать текст, чтобы стало понятнее.