Структурите в .NET Framework представляват съвкупност от полета с данни. Te приличат много на класовете, но за разлика то тях са стойностни типове. Инстанциите на структурите имат поведение като примитивните числени типове – разполагат в стека за изпълнение на програмата, предават се по стойност и се унищожават при излизане от обхват.
За разлика от структурите класовете са типове по референция и се разполагат в динамичната памет заради което създаването и унищожаването им е по-бавно. При предаване като параметри се предава само техният адрес в динамичната памет (т. нар. референция).
Структурите, както и класовете, могат да дефинират конструктори, полета, свойства, индексатори и други членове.
Въпреки, че синтаксисът на езика C# го допуска, не се препоръчва в структурите да има методи с логика. Структурите трябва да се използват за да съхраняват някаква структура от данни (съвкупност от полета).
При правилна употреба заместването на класове със структури може значително да увеличи производителността. Ще се спрем по-подробно на класовете и структурите в темата "Обща система от типове".
Структурите се дефинират по същия начин, както и класовете, но вместо запазената дума "class" се използва запазената дума "struct".
Структури – пример
За да демонстрираме работата със структури, ще дадем няколко примера:
struct Point
{
public int mX, mY;
}
struct Color
{
public byte mRedValue;
public byte mGreenValue;
public byte mBlueValue;
}
struct Square
{
public Point mLocation;
public int mSize;
public Color mBorderColor;
public Color mSurfaceColor;
}
|
Както виждаме, структурите много приличат на класове, но основното им предназначение е да съхраняват данни.
Предаване на параметрите
В C# има три различни режима на предаване на параметрите. Ще ги разгледаме накратко, след което ще се спрем по-подробно на всеки от тях и ще илюстрираме разликите между тях с примери. Параметрите при извикване на метод могат да се предават по следните начини:
-
out (изходни параметри за връщане на стойност)
Параметрите могат да не бъдат инициализирани преди предаването им. Инициализацията се извършва от извиквания метод, а преди нея достъпът е само за писане и в тялото на метода, и в кода, който го извиква.
-
ref (входно-изходни параметри за предаване по референция)
Промените, които методът прави по подадените му по референция параметри, изменят истинските стойности на параметрите, а не техни копия от стека, и за това са видими от кода, извикал метода.
-
in (входни параметри за предаване по стойност)
Това е подразбиращият се режим на предаване на параметрите в C#. При изпълнението на метода в стека се записват стойностите на параметрите, с които методът работи и след излизането от тялото му, когато се изтрие върха на стека, промените в параметрите остават изгубени, като стойността на локална променлива, излязла от обхват.
Параметри за връщане на стойност (out)
Предаването на out параметри се задейства, като маркираме параметъра с ключовата дума out, и в дефиницията на метода, и при извикването му. Целта на тези параметри е не методът да приема като входни данни тяхната стойност, а единствено да я инициализира и да я върне като резултат от изпълнението си. По тази причина те се наричат изходни параметри.
Изходните параметри се предават по адрес в случай на стойностен тип и по адрес на референцията (двоен указател) в случай на референтен тип. Благодарение на това при промяна на стойността им в даден метод тази промяна директно се отразява на променливата, подадена от извикващия метод.
Тъй като върнатата стойност от даден метод в C# може да бъде само една, използвайки out параметри можем да върнем на кода, извикал метода, повече стойности. Нека разгледаме следния пример за да илюстрираме връщането на стойност чрез out параметри:
public struct Point
{
public int mX, mY;
public Point(int aX, int aY)
{
mX = aX;
mY = aY;
}
}
public struct Dimensions
{
public int mWidth, mHeight;
public Dimensions(int aWidth, int aHeight)
{
mWidth = aWidth;
mHeight = aHeight;
}
}
public class Rectangle
{
private int mX, mY, mWidth, mHeight;
public Rectangle(int aX,int aY, int aWidth,int aHeight)
{
mX = aX;
mY = aY;
mWidth = aWidth;
mHeight = aHeight;
}
public void GetLocationAndDimensions(
out Point aLocation, out Dimensions aDimensions)
{
aLocation = new Point(mX, mY);
aDimensions = new Dimensions(mWidth, mHeight);
}
}
class TestOutParameters
{
static void Main()
{
Rectangle rect = new Rectangle(5, 10, 12, 8);
Point location;
Dimensions dimensions;
// location and dimension are not previously initialized
rect.GetLocationAndDimensions(
out location, out dimensions);
Console.WriteLine("({0}, {1}, {2}, {3})",
location.mX, location.mY,
dimensions.mWidth, dimensions.mHeight);
// Result: (5, 10, 12, 8)
}
}
|
В горния пример са дефинирани структурите Point и Dimensions, които методът GetLocationAndDimensions(…) на класа Rectangle използва за да връща чрез изходните си параметри техни инстанции.
Трябва да обърнем внимание на употребата на ключовата дума out и на това, че променливите location и dimensions не са инициализирани никъде в тялото на метода Main(…). Ако параметрите не бяха указани като такива за връщане на стойност, това не би било допустимо – получава се грешка при компилация "Use of unassigned local variable".
Примерът извлича с едно извикване на метод две стойности – местоположението и размерите на даден правоъгълник, като ги записва в инстанции на структурите Point и Dimensions.
Предаване по референция (ref)
Предаването на параметрите по референция се активира като добавим ключовата дума ref към описанието на даден параметър в дефиницията на метода и при извикването му. Такива параметри се наричат входно-изходни.
При предаване на параметри по референция при стартиране на метода в стека не се записват копия на стойностите на параметрите, а указатели към адреса в паметта на оригиналните им стойности. Така извиканият метод може както да чете информация от подадените му параметри, така и да ги изменя и да връща стойности на кода, който го е извикал.
Параметрите поп референция се предават по адрес в случай на стойностен тип и по адрес на референцията (двоен указател) в случай на референтен тип. Благодарение на това при промяна на стойността им в даден метод тази промяна директно се отразява на променливата, подадена от извикващия метод. Ще илюстрираме това с пример:
public struct Point
{
internal int mX, mY;
public static void IncorrectMultiplyBy2(Point aPoint)
{
aPoint.mX *= 2; aPoint.mY *= 2;
}
public static void MultiplyBy2(ref Point aPoint)
{
aPoint.mX *= 2; aPoint.mY *= 2;
}
static void Main()
{
Point p = new Point();
p.mX = 5;
p.mY = -8;
Console.WriteLine("p=({0},{1})", p.mX, p.mY); // 5,-8
IncorrectMultiplyBy2(p);
Console.WriteLine("p=({0},{1})", p.mX, p.mY); // 5,-8
MultiplyBy2(ref p);
Console.WriteLine("p=({0},{1})", p.mX, p.mY); // 10,-16
}
}
|
При изпълнение на примера се вижда, че в тялото на метода Main(…) не се отразяват промените в предадения в подразбиращия се режим (в случая по стойност) параметър p при извикването на метода IncorrectMultiplyBy2(…). Когато, обаче, параметърът е маркиран като ref, методът MultiplyBy2(…) успява да удвои членовете му, тъй като този метод променя директно подадената стойност, а нейно копие.
Предаване по стойност (in)
В подразбиращия се режим при извикване на метод му се подават копия от стойностите на параметрите. Реално стойностните типове се предават по стойност (предава се тяхно копие), а референтните типове се предават по референция (предава се копие на тяхната адреса в динамичната памет, към който сочат).
В по-горния пример видяхме как въпреки промяната в тялото на метода IncorrectMultiplyBy2(…) предадената по стойност променлива p не измени реалната си стойност. Трябва да обърнем внимание, че p е от стойностен тип (инстанция на структурата Point). Ако p беше референтен тип, промените в членовете му щяха да бъдат видими за кода, извикал метода. Защо това е така, въпреки че предаваме параметъра по стойност? На този въпрос ще си отговорим след като разгледаме следващия пример:
public class ClassPoint
{
internal int mX, mY;
public static void MultiplyBy2(ClassPoint aPoint)
{
aPoint.mX *= 2; aPoint.mY *= 2;
}
public static void IncorrectErase(ClassPoint aPoint)
{
aPoint = null;
}
static void Main()
{
ClassPoint p = new ClassPoint();
p.mX = 5;
p.mY = -8;
Console.WriteLine("p=({0},{1})", p.mX, p.mY); // 5,-8
MultiplyBy2(p);
Console.WriteLine("p=({0},{1})", p.mX, p.mY); // 10,-16
IncorrectErase(p);
Console.WriteLine("p=({0},{1})", p.mX, p.mY); // 10,-16
}
}
|
Забелязваме, че при обръщението към метода MultiplyBy2(…) дори и без да указваме, че параметърът се предава по референция, полетата на p успешно се удвояват. Това е така, защото класът ClassPoint е референтен тип и променливата от този тип представлява указател към паметта, където е записана същинската стойност на обекта.
При извикване на метод с предаване на параметрите по стойност в стека се прави копие на подадената променлива, която в случая е указател (референция) и операциите с това копие изменят реално оригиналната стойност на променливата в динамичната памет.
При промяна на даден параметър, подаден по стойност, например при изменянето на параметъра aPoint в тялото на метода IncorrectErase(…), се изменя единствено копираният на стека указател, а не реалната стойност на обекта. Затова при излизане от метода IncorrectErase(…), въпреки че за параметъра aPoint е зададена стойност null, променливата p не е променена и сочи към обекта, който е бил подаден при извикването.
Променлив брой параметри
В C# можем да дефинираме методи с променлив брой параметри. Пример за такъв метод, който неведнъж сме ползвали в нашите примери, е Console.WriteLine(…). Предаването на променлив брой параметри в C# се реализира чрез следния синтаксис:
static int Sum(params int[] aValues)
{
int retval = 0;
foreach(int arg in aValues)
{
retval += arg;
}
return retval;
}
static void Main()
{
int sum = Sum(1, 2, 3, 4, 5);
Console.WriteLine(sum); // The result is 15
}
|
В горния пример дефинирахме метода Sum(…), който изчислява сумата на произволен брой цели числа от тип int. Указваме, че методът приема произволен брой параметри със служебната дума params. Тя може да се използва най-много веднъж в дефиницията на даден метод и задължително се прилага към последния изреден параметър, който трябва да бъде масив, приемащ множеството от параметрите. На метода от примера могат да бъдат подадени както произволен брой променливи от тип int, така и масив от тип int, т. е. допустими извиквания са както Sum(1,2,3), така и Sum(new Object[]{1,2,3}).
Предаване на променлив брой параметри от различен тип
В някои случаи е възможно да се нуждаем от метод, който да приема произволен брой параметри, но не задължително от един и същ тип. Например такъв би могъл да бъде методът, който изчислява сума на произволен брой целочислени параметри, включително и такива, зададени като символни низове. За този метод допустими обръщения биха били sum(1, 2, 3) както и sum(1, "2", 3).
За да реализираме такъв метод, можем да предаваме параметрите чрез масив от по-общ тип, например чрез масив от инстанции на типа System.Object, който е базов тип за всички типове в .NET Framework. Така можем да работим с произволно множество от параметри, чиито тип различаваме с помощта на оператора за принадлежност към тип is. В нашия случай за да използваме параметри от тип символен низ, първо ги конвертираме към желания целочислен тип. Ето примерна реализация на описаната идея:
int Sum(params Object[] values)
{
int retval = 0;
foreach(Object arg in values)
{
if (arg is int)
{
retval += (int)arg;
}
else if (arg is string)
{
retval += int.Parse((string)arg);
}
}
return retval;
}
|
В този вариант методът Sum(…) извлича целочислената стойност от низа, когато се натъкне на такъв. Забелязваме употребата на оператора is, който връща true ако първият му аргумент "е" от типа, подаден като втори аргумент и false в противен случай.
В примера сме използвали операциите (int)arg и (string)arg, които наричаме преобразуване на типове. На тях ще се спрем след малко, когато разглеждаме предефинирането на оператори и наследяване.
Сподели с приятели: |