https://habr.com/ru/post/447478/- C++
- Go
- Java
- Python
- Ненормальное программирование
Вообще я C++ программист. Ну так получилось. Подавляющее большинство коммерческого кода, который я написал за свою карьеру, — это именно C++. Мне не очень нравится такой сильный перекос моего личного опыта в сторону одного языка, и я стараюсь не упускать возможности написать что-нибудь на другом языке. И мой текущий работодатель внезапно такую возможность предоставил: я взялся сделать одну не самую тривиальную утилиту на Java. Выбор языка реализации был сделан по историческим причинам, да я и не возражал. Java так Java, чем менее мне знакомо — тем лучше.
Помимо прочего возникла у меня довольно простая задача: единожды сформировать некий набор логически связанных данных и передать его некоему потребителю. Потребителей может быть несколько, и согласно принципу инкапсуляции передающий код (производитель) понятия не имеет, что там внутри и что оно может сделать с исходными данными. Но производителю надо, чтобы каждый потребитель получил одни и те же данные. Делать копии и отдавать их мне не хотелось. Значит, надо как-то лишить потребителей возможности изменять переданные им данные.
Тут-то моя неопытность в Java и дала о себе знать. Мне не хватало возможностей языка по сравнению с C++. Да, тут есть ключевое слово
final
, но
final Object
— это как
Object* const
в C++, а не
const Object*
. Т.е. в
final List<String>
можно добавлять строки, например. То ли дело C++: понавставлять везде
const
по заветам Майерса, и все! Никто ничего не изменит. Так? Ну не совсем. Я немного поразмышлял на эту тему
вместо того, чтобы делать ту утилиту на досуге, и вот к чему я пришел.
С++
Напомню саму задачу:
- Единожды создать набор данных.
- Ничего не копировать без надобности.
- Запретить потребителю менять эти данные.
- Минимизировать код, т.е. не создавать кучу методов и интерфейсов для каждого набора данных, нужного в общем-то всего в паре мест.
Никаких отягчающих условий типа многопоточности, безопасности в смысле исключений и т.п. Рассмотрим самый простой случай. Вот как бы я это сделал с использованием наиболее знакомого мне языка:
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.cppvoid 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.cppvoid 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.cppvoid 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.Collections
—
unmodifiableList
,
unmodifiableMap
и т.д. Т.е. интерфейс у константных и неконстантных объектов один и тот же, но неконстантные кидают исключение при попытке их изменить.
Что же касается пользовательских типов, то самому пользователю и придется создавать неизменяемые обертки. В общем, вот мой вариант для Java.
Foo.javapackage 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.javapackage 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.javaimport 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.javaprivate 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.javaimport 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.pyclass 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.pyfrom 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.pyfrom 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.pyfrom 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.pydef 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.pydef 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.gopackage 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.gopackage 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.gopackage 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.
Если кому-то интересны использованные примеры кода, их можно взять
здесь.