Итак, как же компилятор позволяет вызывать метод с помощью стандартного синтаксиса
объект.поле!
И откуда берется переменная
value"!
Чтобы ответить на эти вопросы, взглянем на MSIL-код, сгенерированный компилятором. Сначала рассмотрим метод-получатель свойства. В следующем примере определен такой метод-получатель:
class Address
{
protected string city; protected string zipCode; public string ZipCode {
get {
return zipCode;
} >
>
Взглянув на MSIL, получившийся из этого метода, вы увидите, что компилятор создал метод-аксессор
getJZipCode,
как показано ниже:
.method public hidebysig specialname instance string. get_ZipCode() cil managed
{
// размер 11 (Oxb)
.maxstack 1
.locals ([0] string _Vb_t_$00000003$00000000)
IL.OOOO: ldarg.0
IL_0001: Idfld string Address::zipCode
IL_0006: stloc.O
IL_0007: br.s IL_0009
IL_0009: ldloo.0
ILJWOa: ret } // конец метода Address::get_ZipCode
Вы можете сообщить имя метода-аксессора, поскольку компилятор добавляет к имени префикс
get_
(в случае метода-получателя) или
set_
(в случае метода-установщика). В результате следующий код разрешается как вызов
get__ZipCode:
Я уже обсудил следующие причины, которые делают полезными свойства:
они предоставляют клиентам более высокий уровень абстракции; они обеспечивают универсальные средства доступа к членам класса с использованием синтаксиса объект.поле; они позволяют классу гарантировать, что может быть выполнена любая дополнительная обработка при изменении или обращении к некоторому полю.
Третий пункт связан с еще одним полезным способом применения свойств — реализации
отложенной инициализации
(lazy initialization). При этой методике оптимизации некоторые члены класса не инициализируются, пока не потребуются.
Отложенная инициализация дает преимущества, если у вас есть класс с членами, на которые редко ссылаются и на инициализацию которых уходит много времени и ресурсов. Примерами этого могут служить ситуации, когда требуется считывание данных из БД или через перегруженную сеть. Поскольку вам известно, что на эти члены ссылаются редко, а их инициализация требует больших ресурсов, их инициализацию можно отложить до вызова их методов-получателей. Чтобы проиллюстрировать этот момент, допустим, что у вас есть приложение управления запасами, которое представители по продажам запускают на своих портативных компьютерах для размещения заказов клиентов, и время от времени используют его для проверки наличия товара. Используя свойства, вы можете разрешить создание экземпляров соответствующих классов, так чтобы при этом не считывались записи из БД, как показано в приведенном ниже коде. Когда представитель захочет узнать о количестве товара на складе, метод-получатель обратится к удаленной БД.
class Sku {
protected double onHand;
public string OnHand {
get
{
// Считать из центральной базы данных и установить
// значение onHand.
return onHand; } } }
Итак, свойства позволяют предоставлять методы-аксессоры для полей и универсальные, интуитивно понятные интерфейсы для клиента. Из-за этого свойства иногда называют "умными полями". А теперь рассмотрим способы определения и использования массивов на С#. Вы также узнаете, как свойства используются с массивами в виде
индексаторов
(indexers).
Этот синтаксис отличен от C++, в котором квадратные скобки идут после имени переменной. Поскольку массивы основаны на классах, многие из правил объявления классов применяются и к массивам. Например, при объявлении массива на самом деле вы не создаете его. Так же, как и в случае класса, вы должны создать экземпляр массива, и только после этого он будет существовать в том смысле, что для его элементов будет выделена память. Вот как объявить массив и одновременно создать его экземпляр:
// Этот код объявляет одномерный массив
// из 6 элементов и создает его экземпляр.
int[] numbers = new int[6]; -
Однако, объявляя массив как член класса, вам нужно разбить определение массива и создание его экземпляра на два четко обозначенных этапа, поскольку вы не можете создавать экземпляр объекта до периода выполнения:
Я уже описал основные поддерживаемые С# типы, способы их объявления и использования в классах и приложениях. А теперь мы нарушим порядок изложения, при котором каждая глава посвящена описанию какой-либо одной из важных функций языка, — в этой главе вы узнаете о свойствах, массивах и индексаторах, так как у этих функций языка много общего. Они позволяют разработчику классов на С# расширять возможности базовых структур классов (полей и методов), чтобы члены классов предоставляли более понятный и естественный интерфейс.
IL_0002: stfld string Address::zipCode IL_0007: ret } // конец метода Address::set_ZipCode
Даже если вы не найдете этого метода в исходном коде на С#, при установке свойства
ZipCode
[например, так:
addr.ZipCode(" 12345")},
оно разрешается в MSIL-вызов метода
Address ::set_ZipCode(" 12345").
Как и в случае метода
getJZipCode,
попытка прямого вызова этого метода на С# приводит к ошибке.
До сих пор мои примеры иллюстрировали способы определения конечного, предопределенного числа переменных. Однако во многих реальных приложениях точное число нужных объектов неизвестно до периода выполнения. Так, если вы разрабатываете редактор и хотите отслеживать число элементов управления, добавляемых к диалоговому окну, точное количество элементов управления, которое будет показано редактором, неизвестно до периода выполнения. Но для хранения и отслеживания совокупности динамически выделяемых объектов, в данном случае — элементов управления редактора — вы можете использовать массив. В С# массивы являются объектами, производными от базового класса
System.Array.
Поэтому, хотя синтаксис определения массива аналогичен C++ или Java, реально вы создаете при этом экземпляр класса .NET. Это значит, что члены каждого объявленного массива унаследованы от
Sys-tem.Array.
В этом разделе я расскажу, как объявлять массивы и создавать их экземпляры, как работать с массивами разных типов, и опишу циклическую обработку элементов массива. Я также коснусь нескольких распространенных свойств и методов класса
System.Array.
Кроме одномерных, С# поддерживает объявление многомерных массивов, где каждое измерение отделяется запятой. Здесь я объявил трехмерный массив двойных слов:
doublet,>1 numbers;
Чтобы быстро определить число измерений массива, объявленного на С#, подсчитайте число запятых и к сумме прибавьте единицу.
В следующем примере я объявил двумерный массив объемов продаж, представляющих объемы продаж по месяцам в этом году и суммы за аналогичный период времени прошлого года. Обратите особое внимание на синтаксис создания экземпляра массива (в конструкторе
MultiDimAirayApp).
using System;
class MultiDimArrayApp
,--~~~~~ {
protected int currentMonth;
protected doublet,] sales;
MultiDimArrayAppO {
currentMonth=10;
sales = new double[2, currentMonth];
for (int i = 0; i < sales.GetLength(O); i++)
{
for (int j=0; j < 10; j++) {
sales[i,j] = (i * 100) + j; } } >
protected void PrintSalesO <
for (int i = 0; i < sales.GetLength(O); i++)
{
for (int j=0; j < sales.GetLength(l); j++) {
Console.WriteLine("[{0}][{1}]={2}", i, j, sales[i,J]); } } }
public static void Main() {
MultiDimArrayApp app = new MultiDimArrayAppO;
app.PrintSalesO;
} }
Запустив
MultiDimArrayApp,
вы получите такую информацию:
[0][0]=0
[0][1]=1
[0][2]=2
[0][3]=3
[0][4]=4
[0][5]=5
[0][6]=6
[0][7]=7
[0][8]=8
С0][9]=9
[1][0]=100
[1][1]=101
[1][2]=102
[1][3]=103
[1][4]=104
[1][5]=105
[1][6]=106
[1][7]=107
[1][8]=108
[1][9]=109
Помните: свойство
Length,
как я говорил при рассмотрении примера одномерного массива, возвращает суммарное число элементов массива, поэтому в данном примере это свойство вернет 20. Для определения длины или верхней границы каждого измерения массива в методе
MultiDimArray.PrintSales я
использовал метод
Array. GetLength.
Далее я смог задействовать каждое конкретное значение в методе
PrintSales.
У свойств, как и методов, могут быть указаны модификаторы
virtual, override
или
abstract,
о которых я рассказывал в главе 6. Это позволяет производным классам наследовать и подменять свойства подобно любому другому члену, унаследованному от базового класса. Главная проблема в том, что вы можете задавать эти модификаторы только на уровне свойства. Иначе говоря, когда у вас есть оба метода — получатель и установщик, при подмене одного из них нужно подменять и второй.
В нашем примере свойство
Address.ZipCode
считается доступным для чтения и записи, так как определены оба метода: установщик и получатель. Конечно, иногда может потребоваться лишить клиент возможности устанавливать значение данного поля. В этом случае вы можете сделать это поле неизменяемым, опустив метод-установщик. Чтобы проиллюстрировать неизменяемые свойства, предотвратим установку поля
Address.city
клиентом, оставив
Address.ZipCode
как единственную ветвь кода, задачей которого является изменение значение поля:
class Address {
protected string city;
public string City {
get
{
return city;
} }
protected string zipCode; public string ZipCode {
get
{
return zipCode;
}
set
{
// Сверить значение с базой данных.
zipCode = value;
// обновить город с помощью проверенного zipCode. } } }
Последнее, что мы рассмотрим в связи с массивами, —
невыровненные массивы
(jagged array). Невыровненный массив — это просто массив массивов. Вот пример определения массива, состоящего из массивов целочисленных значений:
int[][] jaggedArray;
Невыровненные массивы можно применять при разработке редактора. При этом вы можете хранить каждый объект, представляющий созданный пользователем элемент управления, в массиве. Допустим, у вас есть массив кнопок и массив полей со списком (чтобы этот пример был небольшим и легко управляемым). У вас могут быть три кнопки и три поля со списком в соответствующих массивах. Объявив невыровненный массив, можно создать для этих массивов "родительский" массив, что позволит вам при необходимости легко выполнять циклическую программную обработку элементов управления:
using System;
class Control {
virtual public void SayHiO
{
Console.WriteLine("Ba3oebiu класс для элементов управления");
} }
class Button : Control {
override public void SayHiO
{
Console.WriteLine("Кнопка");
} }
class Combo ; Control <
override public void SayHiO ?
{ . . ,
Console.WriteLine("Элемент со списком");
} }
class JaggedArrayApp
{
public static void Main() {
Control[][] controls; controls = new Control[2][];
controls[0] = new Control[3];
for (int 1=0; i < controls[0].Length; i++)
<
oontrols[0][i] = new ButtonO;
}
controls[1] = new Control[2];
for (Int i = 0; i < controls[1].Length; i++)
{
controls[1][i] = new ComboO; }
for (int i = 0; i < controls.Length;i++) {
for (int j=0;j< controls[i].Length;J++) <
Control control = controls[i][j]; control. SayHiO; } }
string str = Console.ReadLineO; } }
Как видите, я определил базовый класс
(Control),
два производных
(Button
и
Combo)
и объявил массив массивов, содержащих объекты
Controls.
Таким образом, я могу хранить в массивах значения определенных типов и благодаря полиморфизму быть уверенным, что, когда наступит время извлечь объект из массива (с помощью объектов,
приведенных к базовому классу),
все будет работать так, как я задумал.
В этом коде не объявлена переменная
value,
но мы все же можем использовать ее для хранения значения, переданного вызывающим кодом, и для установки защищенного поля
zipCode.
Генерируя MSIL для метода-установщика, компилятор С# вводит эту переменную как аргумент метода
set_ZipCode.
В сгенерированном MSIL этот метод принимает как аргумент строковую переменную:
.method public hidebysig specialname instance void
set_ZipCode(string 'value') cil managed {
Для объявления массива на С# нужно поместить пустые квадратные скобки между именем типа и переменной, например, так:
Методы-аксессоры используются программистами на нескольких объектно-ориентированных языках, в том числе на C++ и Java. Однако С# предоставляет еще более мощный механизм — свойства с такими же возможностями, как методы-аксессоры, но гораздо более элегантные на стороне клиента. Свойства позволяют написать клиент, способный обращаться к полям класса, как если бы они были открытыми, даже не зная, существуют ли методы-аксессоры.
Свойство в С# состоит из объявления поля и методов-аксессоров, применяемых для изменения значения поля. Эти методы-аксессоры называются
получатель
(getter) и
установщик
(setter). Методы-получатели используются для получения значения поля, а установщики — для его изменения. Вот наш пример, переписанный с применением свойств С#:
class Address {
protected string city;
protected string ZipCode;
public string ZipCode {
get <
return zipCode; }
set {
// Сверить почтовый индекс с базой данных. zipCode = value;
// обновить город с помощью проверенного zipfied*. ^ } } }
Я создал поле
Address.zipCode
и свойство
Address.ZipCode.
Поначалу это может ввести в заблуждение: можно подумать, что
Address.ZipCode
— это поле, да еще и определенное дважды. Но это не поле, а свойство, представляющее собой универсальное средство определения аксессоров для членов класса, что позволяет использовать более интуитивно понятный синтаксис вида
объект, поле.
Если бы я опустил в этом примере поле
Address.zipCode
и изменил бы оператор в установщике с
zipCode = value
на
ZipCode = value,
метод-установщик вызывался бы в итоге бесконечно. Заметьте также, что установщик не принимает аргументов. Передаваемое значение автоматически помещается в переменную
value,
доступную внутри метода-установщика (вскоре с помощью MSIL вы увидите, как происходит это маленькое чудо).
А теперь, написав свойство
Address.ZipCode,
рассмотрим изменения, которые необходимо сделать для клиентского кода:
Address addr = new AddressO;
addr.ZipCode = "55555";
string zip = addr.ZipCode;
Как видите, способ обращения клиента к полям интуитивно понятен: не требуется больше никаких догадок или поисков в документации (и в исходном коде), чтобы узнать, является ли это поле открытым, и, если нет, — выяснять имя метода-аксессора.
Свойства иногда называются "умными полями", а индексаторы — "умными массивами", а значит, стоит использовать для них один синтаксис. Действительно, определение индексатора во многом напоминает определение свойств, кроме двух крупных отличий. Во-первых, индексатор принимает аргумент
индекс.
Во-вторых, поскольку сам класс применяется как массив, в качестве имени индексатора используется ключевое слово
this.
Вскоре вы увидите более полный пример, а сейчас взгляните на такой пример индексатора:
class MyClass
{
public object this [int idx]
{
get
{
// Возврат нужных данных.
} set
{
// Установка нужных данных.
} }
}
Это лишь часть примера, иллюстрирующего синтаксис индексаторов, так как внутренняя реализация способа определения данных, их получения и установки к индексаторам не относится. Имейте в виду, что независимо от внутреннего способа хранения ваших данных (т. е. в виде массива, набора и т. д.) индексаторы — всего лишь средства, позволяющие программисту создавать экземпляр класса для написания, например, такого кода:
MyClass els = new MyClassO;
cls[0] = someObject;
Console.WriteLine("{0}", cls[0]);
Что именно вы делаете в пределах индексатора — ваше личное дело, пока клиент класса получает при обращении к объекту как к массиву ожидаемые результаты.
Однако в этом случае код не будет скомпилирован, так как явно вызывать внутренний метод MSIL недопустимо.
Ответ на наш вопрос — как компилятор позволяет использовать стандартный синтаксис
объект.поле
для вызова метода? — в том, что при разборе синтаксиса свойства на С# компилятор на самом деле генерирует для нас соответствующие методы-получатели и установщики, поэтому в случае свойства
Address.ZipCode
компилятор генерирует MSIL, содержащий методы
get_ZipCode
и
setJZipCode.
А теперь посмотрим на сгенерированный метод-установщик. В классе
Address
вы видели следующее:
Свойства в С# состоят из объявления полей и методов-аксессоров. Свойства обеспечивают интеллектуальный доступ к полям класса, что освобождает программиста, занятого написанием клиента класса, от необходимости выяснять, был ли создан для поля класса метод-аксессор (и если был, то как). На С# массивы объявляются путем размещения пустых квадратных скобок
между
именем типа и переменной — в этом плане синтаксис несколько отличается от принятого в C++. Массивы С# могут быть одномерными, многомерными или невыровненными. В С# с объектами можно обращаться как с массивами, применяя индексаторы. Индексаторы позволяют программистам легко работать с множеством объектов одного типа.
Индексаторы — пример того, как добавленная к языку командой разработчиков небольшая, но мощная функция помогает повысить производительность наших усилий по разработке. Однако, как и у любой функции любого языка, у индексаторов своя область применения. Их нужно использовать только там, где понятно, что с данным объектом можно обращаться, как с массивом. Рассмотрим приложение по подготовке счетов. В нем, конечно, нужен класс
Invoice,
определяющий член-массив объектов
InvoiceDetail.
В таком случае пользователю будет совершенно понятно применение следующего синтаксиса при обращении к подробностям счетов:
InvoiceDetail detail = invoice[2]; // Возвращает 3-ю строку
// с подробностями счета.
Однако этого не скажешь о попытке превращения всех членов
InvoiceDetail
в массив, доступ к которому будет осуществляться через индексатор. Как видите, первая строка гораздо понятнее, чем вторая:
TermCode terms = invoice.Terms; // Аксессор свойства для члена Terms. TermCode terms = invoice[3]; // Тут есть над чем задуматься.
Истина в том, что не стоит делать что-то лишь потому, что это возможно. Или конкретнее: задумывайтесь над тем, как реализация любой новой функции повлияет на клиенты вашего класса, и принимайте исходя из этого решение о том, облегчит ли реализованная функция использование вашего класса, или нет.
Когда же применение индексаторов наиболее оправданно? Начну с уже приведенного примера окна со списком. С концептуальной точки зрения, такое окно представляет собой просто список, или массив строк, подлежащих выводу. В следующем примере я объявил класс с именем
MyListBox,
содержащий индексатор для установки и получения строк через объект
ArrayList
(класс
Array List
является классом .NET Framework, который используется для хранения совокупности объектов).
using System;
using System.Collections;
class MyListBox {
protected ArrayList data = new ArrayListQ;
public object this[int idx] {
get {
if (idx > -1 && idx < data.Count) {
return (data[idxj); }
else {
// Здесь возможно возникновение исключения, return null; } }
set {
if (idx > -1 && idx < data.Count) {
datafidx] = value; >
else if (idx == data.Count) {
data.Add(value);
}
else
{
// Здесь возможно возникновение исключения.
} > } }
class IndexerslApp
{
public static void Main()
{
MyListBox Ibx = new MyListBoxQ;
lbx[0] = "foo";
lbx[1] = "bar";
lbx[2] = "baz";
Console.WriteLine("{0} {1} {2}",
lbx[0], lbx[1], lbx[2]);
} >
В этом примере я реализовал проверку на ошибки, возникающие при выходе индекса за границы. Формально это не связано с индексаторами, поскольку индексаторы связаны лишь со способом использования объекта как массива клиентом класса, и никак — с внутренним представлением данных. Однако при изучении функций нового языка это помогает понять не только их синтаксис, но и принципы практического использования. Итак, в обоих методах индексатора (получателе и установщике) я проверял передаваемое значение индекса с помощью данных, хранимых членом класса
ArrayList.
Лично я выбрал бы генерацию исключений в тех случаях, когда переданное значение индекса не может быть использовано. Но это дело вкуса — ваша обработка ошибок может отличаться. Важно дать знать клиенту о возникновении ошибки, когда передается неверный индекс.
Вот простой пример объявления одномерного массива как члена класса. При этом в конструкторе создается и заполняется экземпляр массива, после чего все элементы массива выводит цикл
using System;
class SingleOimArrayApp {
protected int[] numbers;
SingleDimArrayApp() {
numbers = new int[6];
for (int 1 = 0; i < 6; i++)
{
numbers[i] = i * i; } }
protected void PrintArrayO {
for (int 1=0; i < numbers.Length; 1++)
{
Console.WriteLine("numbers[{0}]={1}", i, numbers[i]); } }
public static void Main() <
SingleDimArrayApp app = new SingleDimArrayAppO;
app. PrintArrayO; } }
При запуске этого примера будет получена выходная информация:
numbers[0]=0
nurabers[1]=1
numbers[2]=4
nuinbers[3]=9
nurabers[4]=16
numbers[5]=25
В этом примере метод
SingleDimArray.PrintArray
определяет число элементов массива с помощью свойства
Length
класса
System.Array.
Это не совсем наглядный пример, так как мы используем всего лишь одномерный массив, а свойство
Length
на самом деле возвращает число
всех
элементов по всем измерениям массива. Так, в случае двумерного массива 5 на 4 свойство
Length
вернет 20. Ниже я рассмотрю многомерные массивы и способы определения верхней границы конкретного измерения массива.
Теперь вы знаете, как объявлять массивы и создавать их экземпляры, как работать с массивами разных типов и циклически обрабатывать элементы массивов. Вы также узнали, как для массивов использовать наиболее популярные свойства и методы, определенные в классе
System.Array.
Теперь перейдем к рассмотрению индексаторов — особой возможности С#, позволяющей программно обращаться с объектами так, как если бы они были массивами.
Но зачем это нужно? Как и в случае большинства функций языка программирования, польза от индексаторов в том, что писать приложения становится более интуитивно понятно. В разделе "Свойства как "умные поля"" вы узнали, как свойства в С# дают вам возможность ссылаться на поля класса с использованием стандартного синтаксиса
класс.поле.
Такие поля в конечном счете приводятся к методам-получателям и установщикам. Это абстрагирование освобождает программиста, пишущего клиент класса, от необходимости определения наличия у поля методов-получателей и установщиков и от необходимости знать их точный формат. Аналогично индексаторы позволяют клиенту класса индексировать объект, как если бы объект был массивом.
Рассмотрим пример. У вас есть класс "окно со списком", куда пользователь класса может вставлять строки. Если вы хорошо знакомы с
Win32
SDK, то знаете, что для того, чтобы вставить строку в окно со списком, нужно послать ему сообщение
LB^ADDSTRING
или
LBJNSERTSTRING.
Когда этот механизм появился в конце 80-х годов, мы думали, что были настоящими объектно-ориентированными программистами. Разве после всего этого мы не посылали сообщения объекту, как нас учили все эти модные книги по объектно-ориентированному анализу и проектированию? Но с началом распространения таких объектно-ориентированных языков и языков на основе объектов, как C++ и Object Pascal, мы узнали, что объекты позволяют создавать более интуитивно понятные интерфейсы программирования для решения подобных задач. При использовании C++ и MFC (Microsoft Foundation Classes) нам доступна вся структура классов, позволяющая обращаться с окнами (такими, как окно со списком), как с объектами, классы которых содержат члены-функции, которые в основном являются тонкими оболочками для отсылки и получения сообщений от элементов управления Microsoft Windows. В случае класса
CListBox
(т. е. оболочки MFC для элемента управления Windows "окно со списком") для решения задач, выполнявшихся прежде путем отсылки сообщений
LB_ADDSTRING
и
LBJNSERTSTRING,
нам даны члены-функции
AddString
и
InsertString.
Приняв это во внимание и стремясь облегчить создание лучшего и наиболее интуитивно понятного языка, команда разработчиков С# задалась вопросом: "А почему бы не предоставить возможность обработки объекта, который по своей сути является массивом, как массива?" Разве окно со списком — это не просто массив строк с дополнительной функциональностью вывода и сортировки? Вот из этой идеи и родилась концепция индексаторов.
zipCode = value;
Всегда поощряется создание классов, которые не только скрывают реализацию своих методов, но и запрещают членам любой прямой доступ к полям класса. Обеспечить корректную работу с полем можно, предоставляя
методы-аксессоры
(accessor methods), выполняющие работу по получению и установке значений этих полей, так чтобы они действовали согласно правилам конкретной предметной области.
Допустим, у вас есть класс "Адрес" с полями для почтового индекса и города. Когда клиент модифицирует поле индекса
Address.ZipCode,
вам нужно сверить введенный код с БД и автоматически установить значение поля
Address. City
в зависимости от этого почтового индекса. Если бы у клиента был прямой доступ к открытому члену
(public) Address.ZipCode,
выполнить обе эти задачи было бы сложно, поскольку для непосредственного изменения открытого члена метод не требуется. Поэтому вместо того, чтобы предоставить доступ к полю
Address.ZipCode,
лучше определить поля
Address.ZipCode
и
Address.City
как
protected
и предоставить методы-аксессоры для получения и установки значения поля
Address.Zip-Code.
Таким образом, вы можете добавить код, выполняющий дополнительную работу при изменении поля.
Этот пример с почтовым индексом можно запрограммировать на С# следующим образом. Заметьте: поле
ZipCode
определено как
protected
и поэтому недоступно клиенту, а методы-аксессоры
GetZipCode
и
SetZipCode
определены
как public.
class Address
{
protected string ZipCode; protected string City;
public string GetZipCodeQ {
return this.ZipCode;
>
public void SetZipCode(string ZipCode)
{
// Сверить почтовый индекс с базой данных.
this.ZipCode = ZipCode;
// Обновить this.City no результатам проверки почтового
// кода. } }
Клиент будет обращаться к значению
Address.ZipCode
примерно так:
Address addr = new AddressQ; addr.SetZipCode("55555"); string zip = addr.GetZipCode();
Теперь, увидев, что динамическая обработка одно- или многомерного массива большой сложности не представляет, вас может заинтересовать способ программного определения числа измерений массива. Число измерений массива называется
рангом,
а его значение позволяет получить свойство
Array.Rank.
Вот как это сделать для нескольких массивов: