فصل ۱۹: برنامهنویسی پویا؛ DLR، DynamicObject و تعامل با زبانهای پویا
فروش یا انتشار این ترجمه منوط به داشتن مجوز لازم از صاحب حقوق اثر است.
نشان تصویری آغاز فصل در منبع
فصل ۱۹: برنامهنویسی پویا (Dynamic Programming)
در فصل ۴ توضیح داده شد که اتصال پویا (dynamic binding) در زبان C# چگونه کار میکند. در این فصل، ابتدا نگاهی کوتاه به زماناجرای زبان پویا (Dynamic Language Runtime یا DLR) میاندازیم و سپس الگوهای زیر را در برنامهنویسی پویا بررسی میکنیم:
- حل سربارگذاری عضو بهصورت پویا (Dynamic member overload resolution)
- اتصال سفارشی، یعنی پیادهسازی اشیای پویا
- تعاملپذیری با زبانهای پویا
نوعهای این فصل در فضای نام System.Dynamic قرار دارند، بهجز CallSite<> که در System.Runtime.CompilerServices قرار گرفته است.
زماناجرای زبان پویا (The Dynamic Language Runtime)
C# برای انجام اتصال پویا به DLR متکی است. برخلاف نامش، DLR نسخهای «پویا» از CLR نیست؛ بلکه کتابخانهای است که روی CLR قرار میگیرد، درست مانند هر کتابخانهٔ دیگری از جمله System.Xml.dll. نقش اصلی آن فراهمکردن سرویسهای زمان اجرا برای یکپارچهکردن برنامهنویسی پویا، هم در زبانهای دارای نوعدهی ایستا و هم در زبانهای دارای نوعدهی پویا است. بنابراین زبانهایی مانند C#، Visual Basic، IronPython و IronRuby همگی از پروتکل واحدی برای فراخوانی پویای توابع استفاده میکنند. در نتیجه میتوانند کتابخانهها را با یکدیگر به اشتراک بگذارند و کدی را که در زبانهای دیگر نوشته شده فراخوانی کنند.
DLR نوشتن زبانهای پویای جدید در .NET را نیز نسبتاً ساده میکند. نویسندگان زبان پویا بهجای آنکه مستقیماً Intermediate Language یا IL تولید کنند، در سطح درختهای عبارت (Expression Trees) کار میکنند؛ همان درختهای عبارتی که در فصل ۸ و فضای نام System.Linq.Expressions بررسی شدند.
DLR همچنین تضمین میکند که همهٔ مصرفکنندگان از مزیت «کشکردن محل فراخوانی» (call-site caching) بهره ببرند. این بهینهسازی مانع تکرار بیدلیل تصمیمهای بالقوه پرهزینهای میشود که هنگام اتصال پویا برای یافتن عضو مناسب گرفته میشوند.
Call Site چیست؟
هنگامی که کامپایلر با یک عبارت dynamic روبهرو میشود، نمیداند در زمان اجرا چه موجودیتی آن عبارت را ارزیابی خواهد کرد. برای نمونه، متد زیر را در نظر بگیرید:
public dynamic Foo (dynamic x, dynamic y)
{
return x / y; // Dynamic expression
}
متغیرهای x و y میتوانند هر شیء CLR، یک شیء COM، یا حتی شیئی میزبانیشده در یک زبان پویا باشند. بنابراین کامپایلر نمیتواند روش ایستای معمول خود را بهکار ببرد و فراخوانی یک متد شناختهشده از یک نوع شناختهشده را منتشر کند. در عوض، کدی تولید میکند که در نهایت به یک درخت عبارت منتهی میشود؛ این درخت عملیات را توصیف میکند و توسط یک call site مدیریت میشود که DLR در زمان اجرا آن را bind میکند. call site در اصل واسطهای میان فراخواننده و مقصد فراخوانی است.
یک call site با کلاس CallSite<> در System.Core.dll نمایش داده میشود. اگر متد پیشین را disassemble کنیم، نتیجه چیزی شبیه کد زیر است:
static CallSite<Func<CallSite,object,object,object>> divideSite;
[return: Dynamic]
public object Foo ([Dynamic] object x, [Dynamic] object y)
{
if (divideSite == null)
divideSite =
CallSite<Func<CallSite,object,object,object>>.Create (
Microsoft.CSharp.RuntimeBinder.Binder.BinaryOperation (
CSharpBinderFlags.None,
ExpressionType.Divide,
/* Remaining arguments omitted for brevity */ ));
return divideSite.Target (divideSite, x, y);
}
همانطور که دیده میشود، call site در یک فیلد static کش میشود تا هزینهٔ ساخت دوبارهٔ آن در هر فراخوانی پرداخت نشود. DLR نتیجهٔ مرحلهٔ binding و مقصدهای واقعی متد را نیز کش میکند؛ با توجه به نوعهای x و y ممکن است چند مقصد وجود داشته باشد.
فراخوانی پویای واقعی با فراخواندن Target در site ــ که یک delegate است ــ و ارسال operandهای x و y انجام میشود.
توجه کنید که کلاس Binder اختصاصی C# است. هر زبانی که اتصال پویا را پشتیبانی میکند binder مخصوص زبان خود را در اختیار DLR قرار میدهد تا عبارتها مطابق قواعد همان زبان تفسیر شوند. برای مثال اگر Foo با اعداد صحیح 5 و 2 فراخوانی شود، binder زبان C# نتیجهٔ 2 را تضمین میکند، در حالی که binder زبان VB.NET نتیجهٔ 2.5 خواهد داد.
حل سربارگذاری عضو بهصورت پویا
فراخواندن یک متد که بهطور ایستا شناخته شده است با آرگومانهایی که نوع پویا دارند، فرایند انتخاب overload را از زمان کامپایل به زمان اجرا موکول میکند. این قابلیت برای سادهسازی برخی وظایف برنامهنویسی ــ از جمله سادهسازی الگوی طراحی Visitor ــ و نیز دورزدن برخی محدودیتهای نوعدهی ایستای C# مفید است.
سادهسازی الگوی Visitor
در اصل، الگوی Visitor امکان میدهد بدون تغییر کلاسهای موجود، متدی را به یک سلسلهمراتب کلاس «اضافه» کنید. شکل ایستای این الگو با وجود سودمندی، نسبت به بسیاری از الگوهای طراحی ظریفتر و کمشهودتر است. همچنین معمولاً لازم است کلاسهای بازدیدشونده با متدی مانند Accept برای visitor آماده شده باشند؛ اگر آن کلاسها تحت کنترل شما نباشند چنین کاری ممکن نیست.
با dynamic binding میتوان همان هدف را سادهتر و بدون تغییر کلاسهای موجود به دست آورد. سلسلهمراتب زیر را در نظر بگیرید:
class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
// The Friends collection may contain Customers & Employees:
public readonly IList<Person> Friends = new Collection<Person> ();
}
class Customer : Person { public decimal CreditLimit { get; set; } }
class Employee : Person { public decimal Salary { get; set; } }
فرض کنید میخواهیم متدی بنویسیم که جزئیات یک Person را بهصورت برنامهای در یک XElement XML صادر کند. راه آشکار این است که در Person متد مجازی ToXElement() تعریف کنیم و آن را در Customer و Employee override کنیم تا CreditLimit و Salary نیز افزوده شوند. این راه دو مشکل دارد:
- ممکن است مالک کلاسهای
Person، Customer و Employee نباشید و نتوانید متد جدیدی به آنها اضافه کنید؛ extension method نیز رفتار polymorphic ایجاد نمیکند. - ممکن است این کلاسها از قبل بزرگ باشند. ضدالگوی رایج «God Object» زمانی رخ میدهد که یک کلاس مانند
Person آنقدر مسئولیت جذب کند که نگهداری آن دشوار شود. بهتر است عملکردهایی که به state خصوصی Person نیاز ندارند به آن اضافه نشوند؛ ToXElement نمونهٔ خوبی است.
با dynamic member overload resolution میتوان این عملکرد را در کلاس جداگانهای نوشت، بدون switchهای ناخوشایند مبتنی بر نوع:
class ToXElementPersonVisitor
{
public XElement DynamicVisit (Person p) => Visit ((dynamic)p);
XElement Visit (Person p)
{
return new XElement ("Person",
new XAttribute ("Type", p.GetType().Name),
new XElement ("FirstName", p.FirstName),
new XElement ("LastName", p.LastName),
p.Friends.Select (f => DynamicVisit (f))
);
}
XElement Visit (Customer c) // Specialized logic for customers
{
XElement xe = Visit ((Person)c); // Call "base" method
xe.Add (new XElement ("CreditLimit", c.CreditLimit));
return xe;
}
XElement Visit (Employee e) // Specialized logic for employees
{
XElement xe = Visit ((Person)e); // Call "base" method
xe.Add (new XElement ("Salary", e.Salary));
return xe;
}
}
متد DynamicVisit یک dispatch پویا انجام میدهد و خاصترین نسخهٔ Visit را که در زمان اجرا تعیین میشود فراخوانی میکند. فراخوانی DynamicVisit برای هر فرد موجود در مجموعهٔ Friends تضمین میکند که اگر دوست موردنظر یک Customer یا Employee باشد overload صحیح انتخاب شود.
var cust = new Customer
{
FirstName = "Joe", LastName = "Bloggs", CreditLimit = 123
};
cust.Friends.Add (
new Employee { FirstName = "Sue", LastName = "Brown", Salary = 50000 }
);
Console.WriteLine (new ToXElementPersonVisitor().DynamicVisit (cust));
خروجی:
<Person Type="Customer">
<FirstName>Joe</FirstName>
<LastName>Bloggs</LastName>
<Person Type="Employee">
<FirstName>Sue</FirstName>
<LastName>Brown</LastName>
<Salary>50000</Salary>
</Person>
<CreditLimit>123</CreditLimit>
</Person>
گونهٔ دیگر Visitor
اگر بیش از یک کلاس visitor لازم دارید، میتوانید یک کلاس پایهٔ abstract تعریف کنید:
abstract class PersonVisitor<T>
{
public T DynamicVisit (Person p) { return Visit ((dynamic)p); }
protected abstract T Visit (Person p);
protected virtual T Visit (Customer c) { return Visit ((Person) c); }
protected virtual T Visit (Employee e) { return Visit ((Person) e); }
}
در این حالت subclassها لازم نیست DynamicVisit را دوباره تعریف کنند؛ فقط نسخههای Visit موردنیاز خود را override میکنند. این ساختار علاوه بر متمرکزکردن متدهای مربوط به سلسلهمراتب Person، امکان فراخوانی طبیعیتر متدهای پایه را فراهم میکند:
class ToXElementPersonVisitor : PersonVisitor<XElement>
{
protected override XElement Visit (Person p)
{
return new XElement ("Person",
new XAttribute ("Type", p.GetType().Name),
new XElement ("FirstName", p.FirstName),
new XElement ("LastName", p.LastName),
p.Friends.Select (f => DynamicVisit (f))
);
}
protected override XElement Visit (Customer c)
{
XElement xe = base.Visit (c);
xe.Add (new XElement ("CreditLimit", c.CreditLimit));
return xe;
}
protected override XElement Visit (Employee e)
{
XElement xe = base.Visit (e);
xe.Add (new XElement ("Salary", e.Salary));
return xe;
}
}
در صورت نیاز میتوان خود ToXElementPersonVisitor را نیز subclass کرد.
فراخوانی ناشناس اعضای یک نوع Generic
سختگیری نوعدهی ایستای C# یک شمشیر دولبه است: از یک سو سطحی از صحت را در زمان کامپایل تحمیل میکند؛ از سوی دیگر گاهی بیان بعضی کدها را دشوار یا ناممکن میسازد و شما را به reflection وادار میکند. در این وضعیت dynamic binding میتواند جایگزینی تمیزتر و سریعتر از reflection باشد.
مثال، زمانی است که باید با شیئی از نوع G<T> کار کنیم اما T را نمیدانیم:
public class Foo<T> { public T Value; }
static void Write (object obj)
{
if (obj is Foo<>) // Illegal
Console.WriteLine ((Foo<>) obj).Value); // Illegal
}
این کد کامپایل نمیشود، زیرا نمیتوان اعضای یک نوع generic نامقید را مستقیماً فراخوانی کرد. نخستین راه پویا این است که عضو Value را بهصورت dynamic بخوانیم:
static void Write (dynamic obj)
{
try { Console.WriteLine (obj.Value); }
catch (Microsoft.CSharp.RuntimeBinder.RuntimeBinderException) {...}
}
Multiple Dispatch
C# و CLR همیشه نوع محدودی از پویایی را با فراخوانی متدهای virtual پشتیبانی کردهاند. تفاوت آن با dynamic binding این است که برای فراخوانی virtual، کامپایلر باید در زمان کامپایل یک عضو virtual مشخص را بر اساس نام و امضای عضو انتخاب کند. بنابراین عبارت فراخوانی باید کاملاً برای کامپایلر شناختهشده باشد و overload resolution نیز بر اساس نوعهای زمان کامپایل آرگومانها انجام شود.
از این رو فراخوانی virtual را «single dispatch» مینامند. در فراخوانی زیر:
animal.Walk (owner);
تصمیم زمان اجرا دربارهٔ انتخاب Walk سگ یا گربه فقط به نوع runtime گیرنده، یعنی animal، بستگی دارد. اگر overloadهای گوناگونی از Walk برای انواع مختلف owner وجود داشته باشند، overload در زمان کامپایل انتخاب میشود و نوع واقعی runtime شیء owner دخالت ندارد.
در مقابل، فراخوانی پویا overload resolution را تا زمان اجرا عقب میاندازد:
animal.Walk ((dynamic) owner);
اکنون انتخاب نهایی متد به نوع runtime هم animal و هم owner وابسته است؛ به این رفتار multiple dispatch گفته میشود.
دسترسی مستقیم dynamic به Value میتواند با هر شیئی که field یا propertyای با این نام دارد کار کند، اما دو اشکال دارد: گرفتن exception هم نامرتب و هم ناکارآمد است و DLR نیز API مستقیمی برای پرسیدن «آیا این عملیات موفق خواهد شد؟» ندارد. دوم آنکه اگر Foo یک interface مانند IFoo<T> باشد و Value بهصورت explicit پیادهسازی شده یا نوع پیادهکننده قابل دسترس نباشد، این روش کار نمیکند.
راه بهتر، ساخت یک helper overloadشده و فراخوانی آن با dynamic member overload resolution است:
static void Write (dynamic obj)
{
object result = GetFooValue (obj);
if (result != null) Console.WriteLine (result);
}
static T GetFooValue<T> (Foo<T> foo) => foo.Value;
static object GetFooValue (object foo) => null;
overload دارای پارامتر object نقش fallback را برای همهٔ نوعها بازی میکند. binder پویای C# در زمان اجرا بهترین overload را انتخاب میکند و اگر شیء از Foo<T> نباشد بهجای exception سراغ overload عمومی میرود. گزینهٔ دیگر حذف fallback و گرفتن RuntimeBinderException است؛ مزیتش تمایز مقدار null واقعی است، ولی هزینهٔ پرتاب و گرفتن exception را دارد.
همان مسئلهای که در فصل ۱۸ برای IGrouping<,> با reflection حل شد، با dynamic سادهتر است:
static string GetGroupKey<TKey,TElement> (IGrouping<TKey,TElement> group)
=> "Group with key=" + group.Key + ": ";
static string GetGroupKey (object source) => null;
public static string ToStringEx (object value)
{
if (value == null) return "<null>";
if (value is string s) return s;
if (value.GetType().IsPrimitive) return value.ToString();
StringBuilder sb = new StringBuilder();
string groupKey = GetGroupKey ((dynamic)value); // Dynamic dispatch
if (groupKey != null) sb.Append (groupKey);
if (value is IEnumerable)
foreach (object element in ((IEnumerable)value))
sb.Append (ToStringEx (element) + " ");
if (sb.Length == 0) sb.Append (value.ToString());
return "\r\n" + sb.ToString();
}
Console.WriteLine (ToStringEx ("xyyzzz".GroupBy (c => c) ));
Group with key=x: x
Group with key=y: y y
Group with key=z: z z z
نکته این است که این راهحل از overload resolution پویا استفاده میکند. دسترسی مستقیم d.Value شکست میخورد، زیرا GroupBy LINQ نوعی داخلی (internal) را برمیگرداند که IGrouping<,> را پیادهسازی میکند. حتی اگر Key public باشد، دسترسپذیری کلاس دربرگیرنده آن را به internal محدود میکند؛ در چنین حالتی دسترسی باید از interface انجام شود و نمیتوان به DLR دستور داد برای فراخوانی پویا مستقیماً همان interface را bind کند.
پیادهسازی اشیای پویا
یک شیء میتواند semantics اتصال خود را با پیادهسازی IDynamicMetaObjectProvider ارائه کند؛ راه سادهتر subclassکردن DynamicObject است که پیادهسازی پیشفرض آن interface را فراهم میکند:
dynamic d = new Duck();
d.Quack(); // Quack method was called
d.Waddle(); // Waddle method was called
public class Duck : DynamicObject
{
public override bool TryInvokeMember (
InvokeMemberBinder binder, object[] args, out object result)
{
Console.WriteLine (binder.Name + " method was called");
result = null;
return true;
}
}
DynamicObject
در مثال بالا TryInvokeMember override شد تا مصرفکننده بتواند متدی مانند Quack یا Waddle را روی شیء پویا فراخوانی کند. DynamicObject متدهای virtual دیگری برای سایر constructهای برنامهنویسی نیز دارد:
| متد | ساختار برنامهنویسی |
TryInvokeMember | متد |
TryGetMember, TrySetMember | Property یا Field |
TryGetIndex, TrySetIndex | Indexer |
TryUnaryOperation | عملگر یکانی مانند ! |
TryBinaryOperation | عملگر دودویی مانند == |
TryConvert | تبدیل یا Cast |
TryInvoke | فراخوانی خود شیء، مانند d("foo") |
این متدها در صورت موفقیت باید true برگردانند. اگر false برگردانند، DLR به binder زبان بازمیگردد و بهدنبال عضو متناظر در خود subclassِ DynamicObject میگردد. اگر آن هم موفق نشود، RuntimeBinderException پرتاب میشود.
نمونهٔ زیر با TryGetMember و TrySetMember دسترسی پویا به attributeهای یک XElement را ممکن میکند:
static class XExtensions
{
public static dynamic DynamicAttributes (this XElement e)
=> new XWrapper (e);
class XWrapper : DynamicObject
{
XElement _element;
public XWrapper (XElement e) { _element = e; }
public override bool TryGetMember (GetMemberBinder binder,
out object result)
{
result = _element.Attribute (binder.Name).Value;
return true;
}
public override bool TrySetMember (SetMemberBinder binder,
object value)
{
_element.SetAttributeValue (binder.Name, value);
return true;
}
}
}
XElement x = XElement.Parse (@"<Label Text=""Hello"" Id=""5""/>");
dynamic da = x.DynamicAttributes();
Console.WriteLine (da.Id); // 5
da.Text = "Foo";
Console.WriteLine (x.ToString()); // <Label Text="Foo" Id="5" />
نمونهٔ مشابه برای System.Data.IDataRecord استفاده از data reader را ساده میکند:
public class DynamicReader : DynamicObject
{
readonly IDataRecord _dataRecord;
public DynamicReader (IDataRecord dr) { _dataRecord = dr; }
public override bool TryGetMember (GetMemberBinder binder,
out object result)
{
result = _dataRecord [binder.Name];
return true;
}
}
...
using (IDataReader reader = someDbCommand.ExecuteReader())
{
dynamic dr = new DynamicReader (reader);
while (reader.Read())
{
int id = dr.ID;
string firstName = dr.FirstName;
DateTime dob = dr.DateOfBirth;
...
}
}
نمونهٔ بعدی TryBinaryOperation و TryInvoke را نشان میدهد:
dynamic d = new Duck();
Console.WriteLine (d + d); // foo
Console.WriteLine (d (78, 'x')); // 123
public class Duck : DynamicObject
{
public override bool TryBinaryOperation (BinaryOperationBinder binder,
object arg, out object result)
{
Console.WriteLine (binder.Operation); // Add
result = "foo";
return true;
}
public override bool TryInvoke (InvokeBinder binder,
object[] args, out object result)
{
Console.WriteLine (args[0]); // 78
result = 123;
return true;
}
}
DynamicObject متدهای virtual دیگری نیز برای زبانهای پویا دارد. بهویژه overrideکردن GetDynamicMemberNames اجازه میدهد فهرست همهٔ نامهای عضوی را که شیء پویا فراهم میکند برگردانید. Debugger ویژوال استودیو نیز برای نمایش نمایی از شیء پویا از همین متد بهره میبرد.
ExpandoObject
یک کاربرد سادهٔ دیگر میتوانست ساخت کلاس پویایی باشد که اشیا را در dictionaryای با کلید رشتهای نگهداری کند؛ این قابلیت از قبل در ExpandoObject وجود دارد:
dynamic x = new ExpandoObject();
x.FavoriteColor = ConsoleColor.Green;
x.FavoriteNumber = 7;
Console.WriteLine (x.FavoriteColor); // Green
Console.WriteLine (x.FavoriteNumber); // 7
ExpandoObject رابط IDictionary<string,object> را پیادهسازی میکند:
var dict = (IDictionary<string,object>) x;
Console.WriteLine (dict ["FavoriteColor"]); // Green
Console.WriteLine (dict ["FavoriteNumber"]); // 7
Console.WriteLine (dict.Count); // 2
تعامل با زبانهای پویا
گرچه C# با کلمهٔ کلیدی dynamic اتصال پویا را پشتیبانی میکند، به این مرحله نمیرسد که بتوانید عبارتی را که در یک رشته توصیف شده در زمان اجرا مستقیماً اجرا کنید:
string expr = "2 * 3";
// We can’t "execute" expr
تبدیل یک رشته به درخت عبارت به parser واژگانی و معنایی نیاز دارد. این امکانات در کامپایلر C# هستند و بهعنوان سرویس زمان اجرا ارائه نمیشوند. در زمان اجرا، C# صرفاً binderای فراهم میکند که به DLR میگوید درخت عبارت از پیش ساختهشده را چگونه تفسیر کند.
زبانهای واقعاً پویا مانند IronPython و IronRuby اجازه میدهند رشتهٔ دلخواهی اجرا شود. این قابلیت برای scripting، سیستمهای پیکربندی پویا و موتورهای قواعد پویا مفید است. بنابراین ممکن است بخش عمدهٔ برنامه در C# نوشته شود، اما برای چنین وظایفی از یک زبان پویا کمک گرفته شود. همچنین ممکن است API موردنیازی در یک زبان پویا وجود داشته باشد و معادل آن در کتابخانههای .NET موجود نباشد.
مثال زیر از IronPython برای ارزیابی عبارتی استفاده میکند که در زمان اجرا از C# ساخته شده است. این کد میتواند مبنای یک ماشینحساب باشد. برای اجرای آن باید بستههای NuGet با نامهای DynamicLanguageRuntime ــ که با System.Dynamic.Runtime اشتباه نشود ــ و IronPython به برنامه اضافه شوند.
using System;
using IronPython.Hosting;
using Microsoft.Scripting;
using Microsoft.Scripting.Hosting;
int result = (int) Calculate ("2 * 3");
Console.WriteLine (result); // 6
object Calculate (string expression)
{
ScriptEngine engine = Python.CreateEngine();
return engine.Execute (expression);
}
چون رشته به Python داده میشود، expression طبق قواعد Python ارزیابی میشود، نه C#. بنابراین میتوان از امکانات زبان Python مانند listها نیز استفاده کرد:
var list = (IEnumerable) Calculate ("[1, 2, 3] + [4, 5]");
foreach (int n in list) Console.Write (n); // 12345
انتقال State میان C# و Script
برای انتقال متغیر از C# به Python چند مرحلهٔ دیگر لازم است. نمونهٔ زیر میتواند مبنای یک rules engine باشد:
// The following string could come from a file or database:
string auditRule = "taxPaidLastYear / taxPaidThisYear > 2";
ScriptEngine engine = Python.CreateEngine ();
ScriptScope scope = engine.CreateScope ();
scope.SetVariable ("taxPaidLastYear", 20000m);
scope.SetVariable ("taxPaidThisYear", 8000m);
ScriptSource source = engine.CreateScriptSourceFromString (
auditRule, SourceCodeKind.Expression);
bool auditRequired = (bool) source.Execute (scope);
Console.WriteLine (auditRequired); // True
با GetVariable میتوان مقدارها را از script نیز برگرداند:
string code = "result = input * 3";
ScriptEngine engine = Python.CreateEngine();
ScriptScope scope = engine.CreateScope();
scope.SetVariable ("input", 2);
ScriptSource source = engine.CreateScriptSourceFromString (code,
SourceCodeKind.SingleStatement);
source.Execute (scope);
Console.WriteLine (scope.GetVariable ("result")); // 6
در مثال دوم SourceCodeKind.SingleStatement بهجای Expression مشخص شد تا engine بداند یک statement باید اجرا شود. نوعها بهطور خودکار بین دنیای .NET و Python marshal میشوند و حتی از سمت script میتوان به اعضای اشیای .NET دسترسی داشت:
string code = @"sb.Append (""World"")";
ScriptEngine engine = Python.CreateEngine ();
ScriptScope scope = engine.CreateScope ();
var sb = new StringBuilder ("Hello");
scope.SetVariable ("sb", sb);
ScriptSource source = engine.CreateScriptSourceFromString (
code, SourceCodeKind.SingleStatement);
source.Execute (scope);
Console.WriteLine (sb.ToString()); // HelloWorld