Лого на 91. НЕГ „Проф. Константин Гълъбов“

Модул 1 · Урок 19

Предаване на аргументи

Референции и какво всъщност пази променлива от клас. Предаване по стойност и по референция, ключовата дума ref и границите ѝ.

Защо ни е това

В предишния урок видя нещо странно: присвояването Rational b = a; не създава копие — двете имена сочат един и същ обект. Този урок обяснява защо, и какво следва от това, когато подаваш обект на метод.

Ключово

Променлива от прост тип пази стойността. Променлива от клас пази адреса на обекта. Оттам идват всички разлики.

Референции

Компилаторът отделя за всяка променлива място в паметта и съпоставя на името ѝ адреса на началния байт на отделеното поле. Така всяка променлива е единство от поле памет и съдържание на полето.

За просто устроените променливи, каквито са целочислените, можем да считаме, че име и адрес са едно и също нещо, а стойността е необходимото ни число.

При сложно устроените променливи обаче — каквито са инстанциите на класове — стойността на променливата не е самата инстанция. Стойността е адресът в паметта, където са разположени атрибутите. Казваме, че стойността на такава променлива е указател или референция.

int i;1024самата стойностCar myCar = new Car();1024адрес, не стойност1024myCar.brandmyCar.modelmyCar.engineVolumemyCar.yearProd
Фиг. 1. Отляво — проста променлива; отдясно — референция, която сочи към полето с атрибутите.

Референция

Стойност на променлива от клас — адресът на обекта в паметта, а не самият обект.

Внимание

Затова Student std; само декларира референция. С това не заделяме памет за самия обект, а само поле, в което може да запишем адрес на такъв обект. Едва new Student() заделя място за нова инстанция.

Предаване по стойност

Когато при извикване на функция подадем като аргумент променлива от примитивен тип, стойността ѝ се копира в отделеното за съответния формален параметър поле и става локална. Такова предаване се нарича по стойност.

Ако променим стойността на формалния параметър вътре във функцията, тази промяна не се отразява на променливата, подадена като аргумент:

public void setEngineVolume(int carEngineVolume)
{ engineVolume = carEngineVolume; carEngineVolume = 0; }

// В главната функция:
int volume = 1000;
myCar.setEngineVolume(volume);
Console.WriteLine(myCar.engineVolume);   // 1000
Console.WriteLine(volume);               // 1000 — не се е променила

Предаването по стойност е удобно, когато искаме функцията да няма възможност да промени стойността на променливата аргумент.

Предаване по референция

Какво става, когато подадем като аргумент променлива от референтен тип, каквито са инстанциите на всички класове? Тогава не се създава копие на инстанцията, а в метода се подава референция към нея. Поради това всички промени, извършени в метода, се запазват в инстанцията и остават видими и за извикващата функция.

public class Engine
{
    public double volume;
    public int power;
    public Engine(double engineVolume, int enginePower)
    { volume = engineVolume; power = enginePower; }
}

public class Car
{
    public string brand, model;
    public Engine engine;
    public Car(string carBrand, string carModel, Engine carEngine)
    {
        brand = carBrand;
        model = carModel;
        carEngine.volume *= 1000;   // от литри в кубически сантиметри
        engine = carEngine;
    }
}

Промяната в конструктора се отразява и на променливата myEngine в главната функция, защото има само един обект от типа Engine и две референции към него — myEngine и myCar.engine.

Примитивен типРеферентен тип
Какво се копираСтойносттаАдресът
Промяна в методаНе се вижда отвънВижда се отвън
Брой обектиДва независимиЕдин, с две имена

Ключовата дума ref

В C# можем да подадем като аргумент и променлива от примитивен тип като референция. Това позволява на метода да промени стойността ѝ — и заобикаля ограничението, че функция може да върне не повече от една стойност.

Думата ref се пише и при дефиницията на метода, пред типа на формалния параметър, и при извикването, пред аргумента:

static void increment(ref int value)
{ value++; }

// При извикването:
int number = 1;
increment(ref number);
Console.WriteLine(number);   // 2

Границата на ref

При обичайното подаване на аргумент от референтен тип промените върху стойността му остават видими за извикващата програма. Но не можем да променим получения аргумент референция да сочи към друг обект — това е съществената разлика между обикновените променливи и променливите референции.

Тук отново на помощ идва ref — с него можем да подадем референция към референция:

public void changeRef(ref Engine changeEngine)
{
    Engine newEngine = new Engine(5, 600);
    changeEngine = newEngine;      // сега сочи друг обект
}

Бележка

Без ref присвояването changeEngine = newEngine щеше да смени само локалното копие на референцията и извикващата програма нямаше да забележи нищо.

Задачи

Упражнения

Задача 1. Напиши конзолно приложение от всеки от примерите в този урок. Компилирай го и наблюдавай резултата от изпълнението.

Задача 2. В примерите от този урок данните са „вградени“ в кода. Направи вариант на всяко от създадените конзолни приложения, в който данните да се въвеждат от конзолата.

Какво трябва да запомниш

  • Променлива от клас пази адрес, не обект. Обектът се появява с new.
  • Примитивните типове се предават по стойност — методът работи с копие.
  • Обектите се предават по референция — промените в метода се виждат отвън.
  • ref прави и примитивен параметър променяем отвън.
  • Пренасочването на референция към друг обект иска ref — иначе се сменя само локалното копие.