А как насчет того, когда Объект внутри типа параметра будет уничтожен?
Я смотрел на использование типа Option.
Это означает преобразование такой функции, как:
Customer GetCustomerById(Int32 customerID) {...}
Customer c = GetCustomerById(619);
DoStuff(c.FirstName, c.LastName);
вернуть опцию Maybe
тип:
Maybe<Customer> GetCustomerById(Int32 customerID) {...}
на моем нефункциональном языке я должен проверить, присутствует ли возвращаемое значение:
Maybe<Customer> c = GetCustomerById(619);
if (c.HasValue)
DoStuff(c.Value.FirstName, c.Value.LastName);
И это работает достаточно хорошо:
- из сигнатуры функции вы знаете, может ли она вернуть
null
(вместо того, чтобы поднимать исключение) - вы ткнули в проверенное возвращенное значение, прежде чем вслепую его использовать
Но нет сборки мусора
Но я не на C#, Java или C++ с его RAII. Я в Дельфи; родной язык с ручным управлением памятью. Я продолжу показывать примеры кода на C# -подобном языке.
С ручным управлением памятью, мой оригинальный код:
Customer c = GetCustomerById(619);
if (c != nil)
{
try
{
DoStuff(c.FirstName, c.LastName);
}
finally
{
c.Free();
}
}
превращается во что-то вроде:
Maybe<Customer> c = GetCustomerById(619);
if (c.HasValue)
{
try
{
DoStuff(c.Value.FirstName, c.Value.LastName);
}
finally
{
c.Value.Free();
}
}
Теперь у меня есть Maybe<>
держась за недействительную ссылку; это хуже, чем ноль, потому что теперь Maybe
считает, что оно имеет допустимое содержимое, а содержимое имеет указатель на память, но эта память недействительна.
Я обменял возможное NullReferenceException
к случайной ошибке повреждения данных.
Кто-нибудь задумывался над этой проблемой и методами ее решения?
Добавить
Я думал о добавлении метода в структуру под названием Free
:
void Free()
{
if (this.HasValue())
{
_hasValue = false;
T oldValue = _value;
_value = null;
oldValue.Free();
}
}
Что работает, если люди называют это; и знаю, как это назвать; знаю, почему это нужно назвать; и знаю, что они не должны звонить.
Много тонких знаний, чтобы избежать опасной ошибки, которую я представил только при попытке использовать тип опции.
Это также разваливается, когда объект, завернутый в Maybe<T>
фактически уничтожается косвенно с помощью метода, не названного каноническим Free
:
Maybe<ListItem> item = GetTheListItem();
if item.HasValue then
begin
DoStuffWithItem(item.Value);
item.Value.Delete;
//item still thinks it's valid, but is not
item.Value.Selected := False;
end;
Бонус Болтовня
Nullable
/ Maybe
/ Option
Тип имеет силу при работе с типами, которые не имеют встроенных не-значений (например, записей, целых чисел, строк, где нет встроенных не-значений).
Если функция возвращает ненулевое значение, то нет способа сообщить о отсутствии возвращаемого результата без использования некоторых специальных значений.
function GetBirthDate(): TDateTime; //returns 0 if there is no birth date
function GetAge(): Cardinal; //returns 4294967295 if there is no age
function GetSpouseName: string; //returns empty string if there is no spouse name
Опция используется для того, чтобы избежать специальных значений часового и сообщать вызывающей стороне, что на самом деле происходит.
function GetBirthDate(): Maybe<TDateTime>;
function GetAge(): Maybe<Integer>;
function GetSpouseName: Maybe<string>;
Не только для ненулевых типов
Option
Тип также приобрел популярность способ избежать NullReferenceExceptions
(или же EAccessViolation
по адресу $00000000), отделяя вещи без вещей.
Функции, возвращающие специальные, иногда опасные, значения часовых
function GetBirthDate(): TDateTime; //returns 0 if there is no birth date
function GetAge(): Cardinal; //returns 4294967295 if there is no age
function GetSpouseName: string; //returns empty string if there is no spouse name
function GetCustomer: TCustomer; //returns nil if there is no customer
Преобразуются в формы, где особые, иногда опасные, дозорные значения невозможны:
function GetBirthDate(): Maybe<TDateTime>;
function GetAge(): Maybe<Integer>;
function GetSpouseName: Maybe<string>;
function GetCustomer: Maybe<TCustomer>;
Вызывающие люди понимают, что функция не может ничего возвратить, и они должны пройти через проверку наличия. В случае типов, которые уже поддерживают нулевое значение, Option
дает нам возможность попытаться остановить людей от создания исключений NullReference.
В функциональных языках программирования это намного надежнее; тип возвращаемого значения может быть создан, поэтому невозможно вернуть nil
- компилятор просто не допустит этого.
В процедурных языках программирования лучшее, что мы можем сделать, это заблокировать nil
и сделать невозможным достичь. И в этом процессе вызывающая сторона имеет более надежный код.
Можно возразить: "Почему бы не сказать разработчику никогда не совершать ошибок":
Плохо
customer = GetCustomer();
Print(customer.FirstName);
Хорошо
customer = GetCustomer();
if Assigned(customer)
Print(customer.FirstName);
Просто получай хорошо.
Проблема в том, что я хочу, чтобы компилятор ловил эти ошибки. Во-первых, я хочу, чтобы ошибки были сложнее. Я хочу пропасть успеха. Это заставляет звонящего понять, что функция может не работать. Сама подпись объясняет, что нужно делать, и облегчает работу с ней.
В этом случае мы неявно возвращаем два значения:
- клиент
- флаг, указывающий, действительно ли клиент там
Люди в функциональных языках программирования приняли концепцию, и это концепция, которую люди пытаются вернуть в процедурные языки, что у вас есть новый тип, который передает, есть ли значение или нет. И попытки использовать его вслепую приведут к ошибке времени компиляции:
customer = GetCustomer();
Print(customer.FirstName); //syntax error: Unknown property or method "FirstName"
Бонус Чтение
Если вы хотите узнать больше о попытках использовать функционал Maybe
Монада на процедурных языках, вы можете проконсультироваться больше мыслей по этому вопросу:
2 ответа
Единственное, что может защитить вас от звонков на что-либо Value
завернут в Maybe
это извлечь ценность из Maybe
переменная очистка Maybe
содержание и использование результата, как вы обычно используете любую ссылку на объект.
Что-то вроде:
TMaybe<T> = record
strict private
FValue: T;
public
...
function ExtractValue: T;
end;
function TMaybe<T>.ExtractValue: T;
begin
if not _hasValue then raise Exception.Create('Invalid operation, Maybe type has no value');
Result := FValue;
_hasValue = false;
FValue := Default(T);
end;
И тогда вы будете вынуждены извлечь значение, чтобы использовать его.
Maybe<ListItem> item = GetTheListItem();
if item.HasValue then
begin
Value := item.ExtractValue;
DoStuffWithItem(Value);
Value.Free;
end;
Вам не нужно Maybe
в Delphi, поскольку объекты являются ссылочными типами, поэтому вы можете использовать nil
указатели объектов, например:
type
TCustomer = class
public
customerID: Int32;
FirstName, LastName: string;
end;
function GetCustomerById(customerID: Int32): TCustomer;
begin
...
if (customerID is found) then
Result := ...
else
Result := nil;
end;
var
c: TCustomer;
begin
c := GetCustomerById(619);
if c <> nil then
DoStuff(c.FirstName, c.LastName);
end;
Если функция должна выделить новый объект для возврата, например:
function GetCustomerById(customerID: Int32): TCustomer;
begin
...
if (customerID is found) then
begin
Result := TCustomer.Create;
...
end else
Result := nil;
...
end;
Затем у вас есть два варианта управления временем жизни (при условии, что вызывающая сторона должна стать владельцем объекта, потому что он не принадлежит в другом месте).
1) можно позвонить Free
когда вы закончите с использованием объекта:
var
c: TCustomer;
begin
c := GetCustomerById(619);
if c <> nil then
try
DoStuff(c.FirstName, c.LastName);
finally
c.Free;
end;
end;
2) вы можете использовать интерфейс с подсчетом ссылок:
type
ICustomer = interface
['{2FBD7349-340C-4A4E-AA72-F4AD964A35D2}']
function getCustomerID: Int32;
function getFirstName: string;
function getLastName: string;
property CustomerID: Int32 read getCustomerID;
property FirstName: string read getFirstName;
property LastName: string read getLastName;
end;
TCustomer = class(TInterfacedObject, ICustomer)
public
fCustomerID: Int32;
fFirstName, fLastName: string;
function getCustomerID: Int32;
function getFirstName: string;
function getLastName: string;
end;
function TCustomer.getCustomerID: Int32;
begin
Result := fCustomerID;
end;
function TCustomer.getFirstName: string;
begin
Result := fFirstName;
end;
function TCustomer.getLastName: string;
begin
Result := fLastName;
end;
function GetCustomerById(customerID: Int32): ICustomer;
begin
...
if (customerID is found) then
begin
Result := TCustomer.Create as ICustomer;
...
end else
Result := nil;
end;
var
c: ICustomer;
begin
c := GetCustomerById(619);
if c <> nil then
DoStuff(c.FirstName, c.LastName);
end;