Перебор элементов в foreach для своих типов (IEnumerable)
Принцип работы оператора foreach такой же, как в C#. Через GetEnumerator() получается перечислитель с функциями Current, MoveNext и Reset. Простой пример:
Foo f=new Foo();
foreach(int item in f) Write(item+" "); //1 2 3 4 5
class Foo{
object GetEnumerator(){
return Enumerator(new MyEnumerator()); //функция Enumerator делает из объекта перечислитель (IEnumerator)
}
}
class MyEnumerator{
int X;
public bool MoveNext(){ //вызывается при каждой итерации (включая первую) в foreach. Должен возвращать true, если перечисление не закончено (текущее значение не последнее), и false если перечисление вышло за пределы последнего элемента в коллекции (последовательности).
return If(++X<=5, true, false); //If(++X<=5, true, false) - то же самое, что ++X<=5 ? true : false. И так и так можно писать.
}
public object Current(){ //возвращает значение текущей итерации
return X;
}
public Reset(){ //сбрасывает значение
X=0;
}
}
В этом примере перечислитель (enumerator) просто возвращает числа от 1 до 5. Перечислитель создаётся функцией Enumerator из любых объектов имеющих функции Current, MoveNext и Reset. MoveNext должна возвращать bool, а Current - object. У функции Enumerator есть перегрузка, которой подаётся не экземпляр перечислителя, а функции. Функцию GetEnumerator можно переписать так:
object GetEnumerator(){
var e=new MyEnumerator();
return Enumerator(e.MoveNext(), e.Current(), e.Reset()); //четвёртым аргументом можно ещё указать метод Dispose
//return EnumeratorByFuncRefs(FuncRef(e.MoveNext()), FuncRef(e.Current()), FuncRef(e.Reset())); //можно так
}
Про Dispose
В отличие от C#, в OverScript foreach не вызывает метод Dispose после завершения или прекращения перечисления. А вот функция ForEach вызывает. Посмотрим на более сложный пример создания списка (list):
var list=new List();
list.Add(10);
list.Add(20);
list.Add(30);
list.ForEach(object item, Write(item)); //Item-0: 10; Item-1: 20; Item-2: 30; Dispose!
WriteLine();
foreach(object $item in list) Write(item); //Item-0: 10; Item-1: 20; Item-2: 30; //как видим, foreach не вызвал Dispose
//символ $ перед item разрешает повторное декларирование переменной (в OverScript нет блочной области видимости)
WriteLine();
WriteLine(Join("", list)); //Dispose! Item-0: 10; Item-1: 20; Item-2: 30;
//Join, чтобы соединить строковые представления элементов, перебирает элементы используя перечислитель
//Dispose вывелся первым потому, что сначала завершилась Join, которая вызвала в конце Dispose перечислителя, и только потом WriteLine вывела результат Join.
class List{
int Capacity;
public int Count;
object[] Items;
New(){
Clear();
}
public Add(int v){
if(Count >= Capacity-1){
Capacity *= 2;
Resize(Items, Capacity);
}
Items[Count]=v;
Count++;
}
public Clear(){
Capacity=10;
Count=0;
Items=new object[Capacity];
}
public Remove(int index){
int c=Count-1;
for(int i=index; i<c; i++) Items[i]=Items[i+1];
Count=c;
}
object GetEnumerator(){
var e=new MyEnumerator(Items, Count);
return Enumerator(e);
}
}
class MyEnumerator{
object[] Items;
int Pos=-1, Count;
New(object[] items, int length){
Items=items;
Count=length;
}
public bool MoveNext(){
return If(++Pos < Count, true, false);
}
public object Current(){
return "Item-"+Pos+": "+Items[Pos]+"; ";
}
public Reset(){
Pos=-1;
}
public Dispose(){
Write("Dispose! ");
}
}
В этом примере перечислитель возвращает не просто значение элемента массива, а строку с этим значением. Если бы нужны были только сами значения, то можно было получить перечислитель так:
object GetEnumerator(){
return Items->GetEnumerator(); //через Reflection получаем перечислитель массива
}
Но в таком случае выводились бы значения не в кол-ве Count, а вообще все (Capacity), что не нужно. Поэтому свой перечислитель тут очень уместен.
В некоторых случаях можно вообще обойтись без класса перечислителя:
Foo f=new Foo(5);
foreach(int x in f) Write(x+" "); //0 1 2 3 4
class Foo{
int Pos=-1;
int Count;
New(int count){
Count=count;
}
object GetEnumerator(){
return Enumerator(GoNext(), GetCurrent(), ResetPos());
}
public bool GoNext(){
return If(++Pos < Count, true, false);
}
public int GetCurrent(){
return Pos;
}
public ResetPos(){
Pos=-1;
}
}
Обратите внимание, что функции Current, MoveNext и Reset названы по-другому. Так можно, когда в функцию Enumerator передаются функции. Тип GetCurrent тут int, а не object, что так же будет работать.
Объект из OverScript можно передать, например, в написанную на C# или VB.NET библиотеку, и этот внешний код сможет работать с ним так же, как с любым другим IEnumerable, вызывая при этом ваши OverScript-функции.
Код библиотеки (C#):
using System;
public class EnumerableTest
{
public static void Start(System.Collections.IEnumerable e)
{
int n = 0;
foreach(object item in e)
Console.WriteLine($"Item-{n++}: " + item);
Console.WriteLine("End!");
}
}
Переделанный пример:
const object lib=typeof(@"C:\tests\TestLib.dll", "EnumerableTest");
Foo f=new Foo(5);
lib->Start(f); //передаём библиотечной функции, которая принимает System.Collections.IEnumerable, экземпляр Foo (формально все экземпляры в OverScript реализуют IEnumerable)
//Item-0: 0
//Item-1: 1
//Item-2: 2
//Item-3: 3
//Item-4: 4
//End!
class Foo{
int Pos=-1;
int Count;
New(int count){Count=count;}
object GetEnumerator(){return Enumerator(GoNext(), GetCurrent(), ResetPos());}
bool GoNext(){return If(++Pos < Count, true, false);}
int GetCurrent(){return Pos;}
ResetPos(){Pos=-1;}
}
Код в библиотеке на каждой итерации foreach вызывает ваши GoNext и GetCurrent. Тут нет ничего особенного, ведь OverScript работает поверх .NET, а вызов ваших функций - это обычный вызов делегатов (Func<bool>, Func<object>, Action), в которых находятся функции интерпретатора, выполняющие код ваших функций.
