Не обнаружил проекты, которые используются в примерах в лекции, также не увидел список задач. |
Система типов языка С#
Семантика присваивания. Преобразования между ссылочными и значимыми типами
Рассматривая семантику присваивания и передачи аргументов, мы обошли молчанием один важный вопрос. Будем называть целью левую часть оператора присваивания, а также формальный аргумент при передаче аргументов в процедуру или функцию. Будем называть источником правую часть оператора присваивания, а также фактический аргумент при передаче аргументов в процедуру или функцию. Поскольку источник и цель могут быть как значимого, так и ссылочного типа, то возможны четыре различные комбинации. Рассмотрим их подробнее.
- Цель и источник значимого типа. Здесь наличествует семантика значимого присваивания. В этом случае источник и цель имеют собственную память для хранения значений. Значения источника заменяют значения соответствующих полей цели. Источник и цель после этого продолжают жить независимо. У них своя память, хранящая после присваивания одинаковые значения.
- Цель и источник ссылочного типа. Здесь имеет место семантика ссылочного присваивания. В этом случае значениями источника и цели являются ссылки на объекты, хранящиеся в памяти ("куче"). При ссылочном присваивании цель разрывает связь с тем объектом, на который она ссылалась до присваивания, и становится ссылкой на объект, связанный с источником. Результат ссылочного присваивания двоякий. Объект, на который ссылалась цель, теряет одну из своих ссылок и может стать висячим, так что его дальнейшую судьбу определит сборщик мусора. С объектом в памяти, на который ссылался источник, теперь связываются, по меньшей мере, две ссылки, рассматриваемые как различные имена одного объекта. Ссылочное присваивание приводит к созданию псевдонимов - к появлению разных имен у одного объекта. Особо следует учитывать ситуацию, когда цель и/или источник имеет значение void. Если такое значение имеет источник, то в результате присваивания цель получает это значение и более не ссылается ни на какой объект. Если же цель имела значение void, а источник - нет, то в результате присваивания ранее "висячая" цель становится ссылкой на объект, связанный с источником.
- Цель ссылочного типа, источник значимого типа. В этом случае "на лету" значимый тип преобразуется в ссылочный. Как обеспечивается двойственность существования значимого и ссылочного типа - переменной и объекта? Ответ прост: за счет специальных, эффективно реализованных операций, преобразующих переменную значимого типа в объект и обратно. Операция " упаковать " (boxing) выполняется автоматически и неявно в тот момент, когда по контексту требуется объект, а не переменная. Например, при вызове процедуры WhoIsWho требуется, чтобы аргумент any был объектом. Если фактический аргумент является переменной значимого типа, то автоматически выполняется операция " упаковать ". При ее выполнении создается настоящий объект, хранящий значение переменной. Можно считать, что происходит упаковка переменной в объект. Необходимость в упаковке возникает достаточно часто. Примером может служить и процедура консольного вывода WriteLine класса Console, которой требуются объекты, а передаются зачастую переменные значимого типа.
- Цель значимого типа, источник ссылочного типа. В этом случае "на лету" ссылочный тип преобразуется в значимый. Операция " распаковать " (unboxing) выполняет обратную операцию, - она "сдирает" объектную упаковку и извлекает хранимое значение. Заметьте, операция " распаковать " не является обратной к операции " упаковать " в строгом смысле этого слова. Оператор obj = x корректен, но выполняемый следом оператор x = obj приведет к ошибке. Недостаточно, чтобы хранимое значение в упакованном объекте точно совпадало по типу с переменной, которой присваивается объект. Необходимо явно заданное преобразование к нужному типу.
Операции "упаковать" и "распаковать" (boxing и unboxing).
Примеры
В нашем следующем примере демонстрируется применение обеих операций - упаковки и распаковки. Поскольку формальный аргумент процедуры Back принадлежит классу Object, то при передаче фактического аргумента значимого типа происходит упаковка значения в объект. Этот объект и возвращается процедурой. Его динамический тип определяется тем объектом памяти, на который указывает ссылка. Когда возвращаемый результат присваивается переменной значимого типа, то, несмотря на совпадение типа переменной с динамическим типом объекта, необходимо выполнить распаковку, "содрать" объектную упаковку и вернуть непосредственное значение. Вот как выглядит процедура Back и тестирующая ее процедура BackTest из класса Testing:
/// <summary> /// Возвращает переданный ему аргумент. /// Фактический аргумент может иметь произвольный тип. /// Возвращается всегда объект класса object. /// Клиент, вызывающий метод, должен при необходимости /// задать явное преобразование получаемого результата /// </summary> /// <param name="any"> Допустим любой аргумент</param> /// <returns></returns> object Back(object any) { return(any); } /// <summary> /// Неявное преобразование аргумента в тип object /// Явное приведение типа результата. /// </summary> public void BackTest() { ux = (uint)Back(ux); WhoIsWho("ux",ux); s1 = (string)Back(s); WhoIsWho("s1",s1); x =(int)(uint)Back(ux); WhoIsWho("x",x); y = (float)(double)Back(11 + 5.55 + 5.5f); WhoIsWho("y",y); }
Обратите внимание, что если значимый тип в левой части оператора присваивания не совпадает с динамическим типом объекта, то могут потребоваться две операции приведения. Вначале нужно распаковать значение, а затем привести его к нужному типу, что и происходит в двух последних операторах присваивания. Приведу результаты вывода на консоль, полученные при вызове процедуры BackTest в процедуре Main.
Две двойственные операции " упаковать " и " распаковать " позволяют, в зависимости от контекста, рассматривать значимые типы как ссылочные, переменные как объекты, и наоборот.