Модераторы: Partizan, gambit
  

Поиск:

Ответ в темуСоздание новой темы Создание опроса
> Виртуальные методы 
:(
    Опции темы
Xenon
Дата 14.11.2007, 22:04 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
***


Профиль
Группа: Участник Клуба
Сообщений: 1529
Регистрация: 12.4.2006

Репутация: 1
Всего: 50



Даже почитав начало Рихтера не совсем понял как именно происходит вызов виртуальных методов именно на НИЗКОМ уровне в .NET. Я так понял там всю дорогу юзается GetType? Только где? 


--------------------
user posted image  
PM MAIL   Вверх
tol05
Дата 15.11.2007, 00:12 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
***


Профиль
Группа: Участник Клуба
Сообщений: 1632
Регистрация: 21.12.2006
Где: Харьков

Репутация: 63
Всего: 170



Оо... Xenon, отличный вопрос...Для начала три тезиса: 

1. Для вызова виртуального метода используется реальный тип объекта, а не тип ссылки, которой он присвоен. Соответственно для невиртуального метода используется тип ссылки, которая на него ссылается.

2. Каждый объект имеет ссылку на внутреннюю сруктуру данных (которая размещается в памяти домена) - т.н. объект-тип. А этот уже объект-тип имеет ссылку на таблицу методов (статических, виртуальных и обычных экземплярных, всех, которыми тип может оперировать)... Пользуясь случаем - еще раз спасибо Рихтеру за науку, дай Бог ему здоровья smile 

3. для вызово невиртуальных методов используется директива call, для виртуальных - callvirt

Ну а теперь все вместе. Допустим, есть
Код

class Base
{
    public virtual void F(){}
}


class Child : Base
{
    public override void F(){}
}

...
Base obj = new Child();
obj.F();


CLR должна найти метод F() и применить его к obj (допустим, что все проверки выполнились, все компилится и все допустимо - для простоты). Как найти метод? 

1. obj - ссылка в стеке на область памяти (в куче), где расположены поля объекта. Нашли этот адрес и залезли в кучу.
2. По нужному адресу находим ссылку на объект-тип (Рихтер писал, что любой объект в куче имеет два члена - syncIndex и указатель на объект-тип). Идем по этому адресу
3. Находим эту служебную структуру и в ней отыскиваем таблицу методов этого типа. Тип у нас - Child и метод F() - переопределен.
4. Вызываем метод F виртуально. 

Если бы F() был не виртуальным методом, то вместо шага 2 (искать адрес объекта-типа в полях объекта) CLR просто взяла бы адрес объекта-типа ссылки на объект (т.е. Base). Вот и отличие callvirt от call.
Вот так, вроде smile... в первом приближении.

GetType() - как раз и указывает на объект-тип.

дополнительно к Рихтеру советую почитать это


--------------------
На хорошей работе и сны хорошие снятся.
PM MAIL   Вверх
Xenon
Дата 15.11.2007, 00:51 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
***


Профиль
Группа: Участник Клуба
Сообщений: 1529
Регистрация: 12.4.2006

Репутация: 1
Всего: 50



Ну еще callvirt от call отличается проверкой ссылки на null.
Да с syncIndex и т.д. все понятно.
То есть получается, что, грубо говоря, для объекта вызывается GetType, узнается реальный тип, смотрится таблица методов по реальному типу, видим там override. Дальше что? Глядем в базовый?
Нечетко я понимаю смысл "вызываем виртуально". Что конкретно происходит в случаях:
Код

class BaseA
{
    public virtual void Foo()
    {
        Console.WriteLine("BaseA Foo");
    }
}

class BaseB : BaseA
{
    public override void Foo()
    {
        Console.WriteLine("BaseB Foo");
    }
}

class BaseC : BaseB
{
    public override void Foo()
    {
        Console.WriteLine("BaseC Foo");
    }
}


Код

class BaseA
{
    public virtual void Foo()
    {
        Console.WriteLine("BaseA Foo");
    }
}

class BaseB : BaseA
{
    public virtual void Foo()
    {
        Console.WriteLine("BaseB Foo");
    }
}

class BaseC : BaseB
{
    public void Foo()
    {
        Console.WriteLine("BaseC Foo");
    }
}


Код

class BaseA
{
    public virtual void Foo()
    {
        Console.WriteLine("BaseA Foo");
    }
}

class BaseB : BaseA
{
    public virtual void Foo()
    {
        Console.WriteLine("BaseB Foo");
    }
}

class BaseC : BaseB
{
    public override void Foo()
    {
        Console.WriteLine("BaseC Foo");
    }
}


При
Код

BaseA obj = new BaseC();
obj.Foo()

?

Узнаем, чтo obj есть BaseC, допустим, видим там override у метода Foo. Значит "нужно вызывать виртуально". Значит мы берем статический тип - в моем случае BaseA и смотрим у него на метод Foo - он помечен как virtual, значит гуляем в производный (BaseB) - там, допустим, тоже virtual - значит идем в BaseC и попадаем в наш тип, там override, значит это Foo относящейся к этой группе методов Foo и вообще мы попали уже в BaseC, поэтому вызываем Foo из BaseC?
А если бы у BaseB был бы метод Foo почен как override? Мы бы пошли опять же, поняли, что у нас obj типа BaseC а статический тип - BaseA. Пошли бы в BaseA - увидели бы virtual, стали бы искать в производном override метод, нашли бы его и успокоившись вызвали?

Добавлено через 2 минуты
Даже если то, что я написал не бред, то все равно интересно именно как CLR гуляет так по классам или это уже слишком мутно? smile


--------------------
user posted image  
PM MAIL   Вверх
tol05
Дата 15.11.2007, 10:39 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
***


Профиль
Группа: Участник Клуба
Сообщений: 1632
Регистрация: 21.12.2006
Где: Харьков

Репутация: 63
Всего: 170



Цитата(Xenon @  14.11.2007,  23:51 Найти цитируемый пост)
Нечетко я понимаю смысл "вызываем виртуально"

Это как раз и значит, что по полю "object-type" объекта ищется объект-тип с нужной таблицей методов. Если бы "вызывали невиртуально", то не нужно было бы для нахождения таблицы методов выяснять real run-time type объекта, достаточно взять "захардкоданный" smile тип ссылки, которая указывает на этот объект...

в твоем случае : никто никуда не гуляет smile . CLR не прыгает по памяти, па таблицам методов, при каждом вызове метода Foo(), не нужно это, поскольку таблицы методов для каждого типа неизменны, определения типов BaseA, BaseB, BaseC не меняются, один раз их загрузили в домен и все.
Код

BaseA obj = new BaseC();
obj.Foo()
BaseA obj1 = new BaseB();
obj1.Foo()
BaseB obj2 = new BaseB();
obj2.Foo()

При запуске приложения метаданные типов считались и создались те пресловутые "внутренние структуры памяти", каждая - со своей таблицей методов. Когда компилировался код, то в местах вызовов Foo() была вставлена директива callvirt (а не call). Когда код работает, то при каждом callvirt ищется таблица методов (реального типа, а не типа ссылки), берется адрес входа в метод и метод выполняется.
Ну а при создании кода метода JIT-ом - для каждого типа создается свой код и адрес точки входа в него записывается в таблицу методов наших трех типов. 

Вот из статьи, линк, на которую я давал
Цитата

Method Slot Table

Таблица слотов методов, встроенная в MethodTable, содержит ссылки на дескрипторы соответствующих методов (MethodDesc), определяющие поведение типа. Method Slot Table создается по линеаризованному списку методов реализации, упорядоченному следующим образом: унаследованные виртуальные методы, добавленные виртуальные методы, методы экземпляра и статические методы.

ClassLoader анализирует метаданные текущего класса, родительских классов и интерфейсов и создает таблицу методов. В процессе формирования таблицы методов он замещает переопределенные виртуальные методы, замещает скрытые методы дочернего класса, создает новые слоты и при необходимости дублирует слоты.

Цитата

Диспетчеризация виртуальных методов
...

При диспетчеризации виртуальных методов всегда используется фиксированный номер слота независимо от того, какие указатели MethodTable используются в данной иерархии реализаций класса (типа). При формировании MethodTable ClassLoader замещает родительские реализации переопределяющими их реализациями дочерних объектов. В результате вызов метода родительского объекта преобразовывается в вызов реализации, определенной в дочернем объекте. 

Одним словом : вся соль - в call и callvirt.

P.S. можно еще вглубь двигаться, заметить, что компилятор может и невиртуальный метод вызывать как виртуальный, через callvirt (здесь такой случай обсуждается), но думаю, что это не нужно. Это уже ньюансы реализации компилятора, оптимизаторов, контекстов использования. И это только запутает тему smile
Рихтер "Вызов виртуальных методов, свойств и событий в CLR"
Цитата

... В CLR есть две инструкции для вызова метода: 
- Инструкция call используется для вызова статических, экземплярных и  виртуальных методов. Если с помощью этой инструкции вызывается статический метод, необходимо указать тип, в котором определяется метод. При вызове экземплярного или виртуального метода необходимо указать переменную, ссылающуюся на объект, причем в call подразумевается, что эта переменная не равна null. Иначе говоря, сам тип переменной указывает, в каком типе определен необходимый метод. Если в типе переменной метод не определен, проверяются базовые типы. Инструкция call часто используется для невиртуального вызова виртуального метода. 
- Инструкция callvirt используется только для вызова экземплярных и виртуальных методов. При вызове необходимо указать переменную, ссылающуюся на объект. Если с помощью этой инструкции вызывается невиртуальный метод экземпляра, тип переменной указывает, где определен необходимый метод. При использовании callvirt для вызова виртуального метода экземпляра CLR определяет настоящий тип объекта, на который ссылается переменная, и вызывает метод полиморфно. При компиляции такого вызова JIT-компилятор генерирует код для проверки значения переменной — если оно равно null, CLR сгенерирует исключение NullReferenceException. Из-за этой дополнительной проверки инструкция callvirt выполняется немного медленнее call. Проверка на null выполняется даже при вызове невиртуального метода экземпляра. 


Это сообщение отредактировал(а) tol05 - 15.11.2007, 13:35


--------------------
На хорошей работе и сны хорошие снятся.
PM MAIL   Вверх
Xenon
Дата 15.11.2007, 16:42 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
***


Профиль
Группа: Участник Клуба
Сообщений: 1529
Регистрация: 12.4.2006

Репутация: 1
Всего: 50



Все равно не понимаю. Получается мы просто получаем реальный тип и вызываем метод, который находим в его метаданных. Так почему тогда если в BaseA сделать метод не виртуальным вызовется Base.Foo()? Да даже если и виртуальный, то все равно, если в BaseB будет просто метод Foo, то мы спотыкнемся. На лицо какая-то связь в виде цепочки, поэтому я и предположил, что CLR гуляет вот так ... 
Цитата
3. Находим эту служебную структуру и в ней отыскиваем таблицу методов этого типа. Тип у нас - Child и метод F() - переопределен.
4. Вызываем метод F виртуально. 

Хорошо, метод переопределен - то есть в BaseB у нас virtual, а в BaseC override. Почему вызывается метод BaseA, если он вообще без меток стоит?


--------------------
user posted image  
PM MAIL   Вверх
tol05
Дата 15.11.2007, 17:17 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
***


Профиль
Группа: Участник Клуба
Сообщений: 1632
Регистрация: 21.12.2006
Где: Харьков

Репутация: 63
Всего: 170



если метод не виртуальный - для его нахождения используется тип ссылки
Код

BaseA obj = new BaseA();
obj.Foo();                                 //Вызовется BaseA.Foo()
BaseA obj1 = new BaseB();
obj1.Foo();                               //Вызовется BaseA.Foo()
BaseA obj2 = new BaseC();
obj2.Foo();                               //Вызовется BaseA.Foo() 


если виртуальный - то для его нахождения используется реальный тип созданного объекта
Код

BaseA obj = new BaseA();
obj.Foo();                                //Вызовется BaseA.Foo()
BaseA obj1 = new BaseB();
obj1.Foo();                               //Вызовется BaseB.Foo()
BaseA obj2 = new BaseC();
obj2.Foo();                               //Вызовется BaseC.Foo()

но объявления типов должно выглядеть так
Код

class BaseA
    {
        public virtual void Foo()
        {
            Console.WriteLine("BaseA Foo");
        }
    }
    class BaseB : BaseA
    {
        public override void Foo()
        {
            Console.WriteLine("BaseB Foo");
        }
    }
    class BaseC : BaseB
    {
        public override void Foo()
        {
            Console.WriteLine("BaseC Foo");
        }
    }


в твоем же коде метод Foo() определен как виртуальный и в BaseA, и в BaseB. Т.е. это два разных метода. Поэтому компилятор и warning выбрасывает - он не знает, переопределить или скрыть Foo() ты хочешь. И цепочка полиморфизма BaseA-BaseC рвется.
Подробно этот случай описан в ECMA 334, параграф 8.3 "Versioning" (здесь)
Класс BaseC переопределяет Foo(), но только Foo() своего базового класса (класса BaseB). Полиморфизм все равно работает, но только для
Код

BaseB obj3 = new BaseB();
obj3.Foo();                               //Вызовется BaseB.Foo()
BaseB obj4 = new BaseC();
obj4.Foo();                               //Вызовется BaseC.Foo()



--------------------
На хорошей работе и сны хорошие снятся.
PM MAIL   Вверх
tol05
Дата 15.11.2007, 17:35 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
***


Профиль
Группа: Участник Клуба
Сообщений: 1632
Регистрация: 21.12.2006
Где: Харьков

Репутация: 63
Всего: 170



вот MSIL
Код

class BaseA
{
    public virtual void Foo()
    {
        Console.WriteLine("BaseA Foo");
    }
}

class BaseB : BaseA
{
    public virtual void Foo()
    {
        Console.WriteLine("BaseB Foo");
    }
}

class BaseC : BaseB
{
    public override void Foo()
    {
        Console.WriteLine("BaseC Foo");
    }
}

Код

TypeDef #2 (02000003)
-------------------------------------------------------
    TypDefName: ConsoleApplication3.BaseA  (02000003)
    Flags     : [NotPublic] [AutoLayout] [Class] [AnsiClass] [BeforeFieldInit]  (00100000)
    Extends   : 01000001 [TypeRef] System.Object
    Method #1 (06000003) 
    -------------------------------------------------------
        MethodName: Foo (06000003)
        Flags     : [Public] [Virtual] [HideBySig] [NewSlot]  (000001c6)
        RVA       : 0x000020b5
        ImplFlags : [IL] [Managed]  (00000000)
        CallCnvntn: [DEFAULT]
        hasThis 
        ReturnType: Void
        No arguments.

TypeDef #3 (02000004)
-------------------------------------------------------
    TypDefName: ConsoleApplication3.BaseB  (02000004)
    Flags     : [NotPublic] [AutoLayout] [Class] [AnsiClass] [BeforeFieldInit]  (00100000)
    Extends   : 02000003 [TypeDef] ConsoleApplication3.BaseA
    Method #1 (06000005) 
    -------------------------------------------------------
        MethodName: Foo (06000005)
        Flags     : [Public] [Virtual] [HideBySig] [NewSlot]  (000001c6)
        RVA       : 0x000020cb
        ImplFlags : [IL] [Managed]  (00000000)
        CallCnvntn: [DEFAULT]
        hasThis 
        ReturnType: Void
        No arguments.

TypeDef #4 (02000005)
-------------------------------------------------------
    TypDefName: ConsoleApplication3.BaseC  (02000005)
    Flags     : [NotPublic] [AutoLayout] [Class] [AnsiClass] [BeforeFieldInit]  (00100000)
    Extends   : 02000004 [TypeDef] ConsoleApplication3.BaseB
    Method #1 (06000007) 
    -------------------------------------------------------
        MethodName: Foo (06000007)
        Flags     : [Public] [Virtual] [HideBySig] [ReuseSlot]  (000000c6)
        RVA       : 0x000020e1
        ImplFlags : [IL] [Managed]  (00000000)
        CallCnvntn: [DEFAULT]
        hasThis 
        ReturnType: Void
        No arguments.

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Код

class BaseA
{
    public virtual void Foo()
    {
        Console.WriteLine("BaseA Foo");
    }
}

class BaseB : BaseA
{
    public override void Foo()
    {
        Console.WriteLine("BaseB Foo");
    }
}

class BaseC : BaseB
{
    public override void Foo()
    {
        Console.WriteLine("BaseC Foo");
    }
}

Код

TypeDef #2 (02000003)
-------------------------------------------------------
    TypDefName: ConsoleApplication3.BaseA  (02000003)
    Flags     : [NotPublic] [AutoLayout] [Class] [AnsiClass] [BeforeFieldInit]  (00100000)
    Extends   : 01000001 [TypeRef] System.Object
    Method #1 (06000003) 
    -------------------------------------------------------
        MethodName: Foo (06000003)
        Flags     : [Public] [Virtual] [HideBySig] [NewSlot]  (000001c6)
        RVA       : 0x000020b5
        ImplFlags : [IL] [Managed]  (00000000)
        CallCnvntn: [DEFAULT]
        hasThis 
        ReturnType: Void
        No arguments.

TypeDef #3 (02000004)
-------------------------------------------------------
    TypDefName: ConsoleApplication3.BaseB  (02000004)
    Flags     : [NotPublic] [AutoLayout] [Class] [AnsiClass] [BeforeFieldInit]  (00100000)
    Extends   : 02000003 [TypeDef] ConsoleApplication3.BaseA
    Method #1 (06000005) 
    -------------------------------------------------------
        MethodName: Foo (06000005)
        Flags     : [Public] [Virtual] [HideBySig] [ReuseSlot]  (000000c6)
        RVA       : 0x000020cb
        ImplFlags : [IL] [Managed]  (00000000)
        CallCnvntn: [DEFAULT]
        hasThis 
        ReturnType: Void
        No arguments.

TypeDef #4 (02000005)
-------------------------------------------------------
    TypDefName: ConsoleApplication3.BaseC  (02000005)
    Flags     : [NotPublic] [AutoLayout] [Class] [AnsiClass] [BeforeFieldInit]  (00100000)
    Extends   : 02000004 [TypeDef] ConsoleApplication3.BaseB
    Method #1 (06000007) 
    -------------------------------------------------------
        MethodName: Foo (06000007)
        Flags     : [Public] [Virtual] [HideBySig] [ReuseSlot]  (000000c6)
        RVA       : 0x000020e1
        ImplFlags : [IL] [Managed]  (00000000)
        CallCnvntn: [DEFAULT]
        hasThis 
        ReturnType: Void
        No arguments.

видно, что для твоего кода создано два слота памяти, для моего - один. Дальше (не беде повторяться smile ) перечитай инфу о слотах из 
Цитата(tol05 @  14.11.2007,  23:12 Найти цитируемый пост)
дополнительно к Рихтеру советую почитать это 




--------------------
На хорошей работе и сны хорошие снятся.
PM MAIL   Вверх
tol05
Дата 16.11.2007, 16:12 (ссылка) | (нет голосов) Загрузка ... Загрузка ... Быстрая цитата Цитата


Эксперт
***


Профиль
Группа: Участник Клуба
Сообщений: 1632
Регистрация: 21.12.2006
Где: Харьков

Репутация: 63
Всего: 170



эпиграф "Как говорится кто о чем, а вшивый - о бане". 

Это снова я. "... Гляжу - а вдоль дороги - мертвые с косами стоят! И тишина ..." smile Как бы никто не отвечает, никто ничего не пишет, а меня эти исследования затянули, остановаится не могу... Так что продолжу smile

Цитата(tol05 @  15.11.2007,  16:35 Найти цитируемый пост)
перечитай инфу о слотах
сам перечитал...

результаты исследования памяти для варианта, сокращенно именуемого как (virtual virtual override)
Код

.load sos.dll
extension C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\sos.dll loaded

!DumpHeap -type BaseC
PDB symbol for mscorwks.dll not loaded
 Address       MT     Size
00b36ccc 002c31b8       12     
total 1 objects
Statistics:
      MT    Count    TotalSize Class Name
002c31b8        1           12 ConsoleApplication3.BaseC
Total 1 objects

!DumpMT -MD 0x002c31b8
EEClass: 002c13e0
Module: 002c2c24
Name: ConsoleApplication3.BaseC
mdToken: 02000005  (E:\TempFolder\WindowsApplication2\ConsoleApplication3\bin\Debug\ConsoleApplication3.exe)
BaseSize: 0xc
ComponentSize: 0x0
Number of IFaces in IFaceMap: 0
Slots in VTable: 7
--------------------------------------
MethodDesc Table
   Entry         MethodDesc           JIT Name
7934cdcc      79137ab8       PreJIT System.Object.ToString()
7934bba0      79137ac0        PreJIT System.Object.Equals(System.Object)
7934bb90      79137ad8       PreJIT System.Object.GetHashCode()
793424c0      79137ae0       PreJIT System.Object.Finalize()
002c30e8      002c3090       NONE ConsoleApplication3.BaseA.Foo()
002c3208      002c31a8       NONE ConsoleApplication3.BaseC.Foo()
002c3218      002c31b0       JIT ConsoleApplication3.BaseC..ctor()


результаты исследования памяти для варианта, сокращенно именуемого как (virtual override override)
Код

.load sos.dll
extension C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\sos.dll loaded

!DumpHeap -type BaseC
PDB symbol for mscorwks.dll not loaded
 Address       MT     Size
00b36ccc 002c31b0       12     
total 1 objects
Statistics:
      MT    Count    TotalSize Class Name
002c31b0        1           12 ConsoleApplication3.BaseC
Total 1 objects

!DumpMT -MD 0x002c31b0
EEClass: 002c13e8
Module: 002c2c24
Name: ConsoleApplication3.BaseC
mdToken: 02000005  (E:\TempFolder\WindowsApplication2\ConsoleApplication3\bin\Debug\ConsoleApplication3.exe)
BaseSize: 0xc
ComponentSize: 0x0
Number of IFaces in IFaceMap: 0
Slots in VTable: 6
--------------------------------------
MethodDesc Table
   Entry        MethodDesc         JIT Name
7934cdcc      79137ab8      PreJIT System.Object.ToString()
7934bba0      79137ac0       PreJIT System.Object.Equals(System.Object)
7934bb90      79137ad8      PreJIT System.Object.GetHashCode()
793424c0      79137ae0      PreJIT System.Object.Finalize()
002c31f8      002c31a0      NONE ConsoleApplication3.BaseC.Foo()
002c3208      002c31a8      JIT ConsoleApplication3.BaseC..ctor()


Я делаю такие выводы:
Цитата

Диспетчеризация виртуальных методов
...
При диспетчеризации виртуальных методов всегда используется фиксированный номер слота независимо от того, какие указатели MethodTable используются в данной иерархии реализаций класса (типа). При формировании MethodTable ClassLoader замещает родительские реализации переопределяющими их реализациями дочерних объектов. В результате вызов метода родительского объекта преобразовывается в вызов реализации, определенной в дочернем объекте. Кроме того, из дизассемблированного кода видно, что диспетчеризация выполняется через слот номер 8, который показывался в отладчике в окне просмотра памяти (рис. 6) и в выводе команды DumpMT.

В версии "virtual virtual override" определено два слота уже в классе BaseB. Причем адрес BaseА.Foo() не замещен, вместо этого добавлен слот BaseB.Foo() со своим адресом.
Когда создается класс BaseC, то при создании его таблицы методов замещается адрес метода BaseB.Foo(), адрес метода BaseА.Foo() копируется, аналогично классу BaseB.
При вызове Foo() объекта BaseC через ссылку на BaseА идет вызов маппинг на слот BaseА.Foo() и вызывается старая версия BaseА.Foo()
При вызове Foo() объекта BaseC через ссылку на BaseB идет вызов маппинг на слот BaseB.Foo(), но при создании BaseC адрес  BaseB.Foo() в его таблице методов был замещен и вызывается версия BaseC.Foo()
Вот почему же при той же сигнатуре классов правильно работает
Код

BaseB obj4 = new BaseC();
obj4.Foo();

ну вот, теперь ИМХО все. smile

Это сообщение отредактировал(а) tol05 - 16.11.2007, 16:13


--------------------
На хорошей работе и сны хорошие снятся.
PM MAIL   Вверх
  
Ответ в темуСоздание новой темы Создание опроса
Прежде чем создать тему, посмотрите сюда:
mr.DUDA
THandle

Используйте теги [code=csharp][/code] для подсветки кода. Используйтe чекбокс "транслит" если у Вас нет русских шрифтов.
Что делать если Вам помогли, но отблагодарить помощника плюсом в репутацию Вы не можете(не хватает сообщений)? Пишите сюда, или отправляйте репорт. Поставим :)
Так же не забывайте отмечать свой вопрос решенным, если он таковым является :)


Если Вам понравилась атмосфера форума, заходите к нам чаще! С уважением, mr.DUDA, THandle.

 
0 Пользователей читают эту тему (0 Гостей и 0 Скрытых Пользователей)
0 Пользователей:
« Предыдущая тема | Общие вопросы по .NET и C# | Следующая тема »


 




[ Время генерации скрипта: 0.1043 ]   [ Использовано запросов: 22 ]   [ GZIP включён ]


Реклама на сайте     Информационное спонсорство

 
По вопросам размещения рекламы пишите на vladimir(sobaka)vingrad.ru
Отказ от ответственности     Powered by Invision Power Board(R) 1.3 © 2003  IPS, Inc.