List<DynamicBusinessObject> dbo = SearchController.Instance.GetSearchResultList(search, null, "date", startRow - 1, ucDataPager1.PageSize, state);
The above line of code is calling the GetSearchResultList method which up until now had 5 arguments.
I added a 6th argument but wanted to make this argument optional so that all the other pages that call this function don't need to be updated just yet.
So i changed the function to look like this:
[System.ComponentModel.DataObjectMethodAttribute(System.ComponentModel.DataObjectMethodType.Select, true)]
public List<DynamicBusinessObject> GetSearchResultList(Search search, List<CategoryAttribute> listCatAttrib, string sortBy, int startRow, int pageSize, [Optional, DefaultParameterValue("")] string state)
{
StorageQuery qry = new QrySearchResult(
search.ID,
(listCatAttrib != null && listCatAttrib.Count > 0) ? listCatAttrib[0].Attribute.ID : -1,
(listCatAttrib != null && listCatAttrib.Count > 1) ? listCatAttrib[1].Attribute.ID : -1,
(listCatAttrib != null && listCatAttrib.Count > 2) ? listCatAttrib[2].Attribute.ID : -1,
1, sortBy, startRow, pageSize, state);
List<DynamicBusinessObject> list = BusinessObject.Search(qry);
return list;
}
However, when i try to build, it gives me the error that GetSearchResultList has no overload method and takes 5 arguments.
I also tried doing string state = "" instead of using [Optional]
Anyone got any ideas why it's complaining about me not passing 6 arguments when i make the call if the 6th argument is optional?
Those attributes are not how you define an optional parameter in c#.
Rather, the syntax is just the parameter plus = <value>.
So your method signature becomes:
public List<DynamicBusinessObject> GetSearchResultList(
Search search,
List<CategoryAttribute> listCatAttrib,
string sortBy,
int startRow,
int pageSize,
string state = "")
Although I'd recommend using null instead of an empty string, the default types are more natural fits if these signatures are consumed by external libraries.
Optional parameters are like constants, they are baked into referencing assemblies at compile-time.
Let's say you have another library which calls this method using the optional parameter, like so:
myObj.GetSearchListResult(search, listCatAttrib, sortBy, startRow, pageSize);
At the time you compile this library, the call will actually be written in your library as:
myObj.GetSearchListResult(search, listCatAttrib, sortBy, startRow, pageSize, "");
Then, if you change the declaration of your method to be:
public List<DynamicBusinessObject> GetSearchResultList(
Search search,
List<CategoryAttribute> listCatAttrib,
string sortBy,
int startRow,
int pageSize,
string state = "some value")
The call in the first library will still pass an empty string, not the new value. You have to recompile the all callers of your method to make sure the new value takes.
That said, this is why if you are exposing this method outside of your assembly, you use overloads instead of default values; if you change the value passed to the method in the overload, you only have to distribute the assembly where the method is defined, you don't have to distribute and have all the callers against that method recompiled.
You should be able to use:
..., string state = "") {
One possible issue is that your project is targeting an older version of .NET. Check the project properties to make sure it is pointing at the correct version.
Related
Let's say I have an interface looks like this.
interface Some {
/**
* Does some on specified array with specified index and returns the array.
*
* #param array the array.
* #param index the index.
* #returns given {#code array}.
*/
byte[] some(byte[] array, int index);
}
Here comes a simple stubbing make the some method just return given array.
Some some = spy(Some.class);
when(some.some(any(), anyInt())
.thenAnswer(i -> i.getArguments(0)};
Is it possible or does it make any sense modifying above code like this?
Some some = spy(Some.class);
byte[] array = any(); // ##?
int index = anyInt(); // ##?
when(some.some(array, index) // ##?
.thenAnswer(i -> i.getArguments(0)};
Does it have same or equivalent effects?
In the code posted, the test passes and the effect is equivalent. This is not true in the general case, and you can easily break the code.
For example this works:
Some some = mock(Some.class);
byte[] any = any();
int index = anyInt();
when(some.some(any, index)).thenAnswer(i -> i.getArguments()[0]);
var res = some.some(new byte[]{1, 2}, 4);
While this with reordered variables does not work:
Some some = mock(Some.class);
int index = anyInt();
byte[] any = any();
when(some.some(any, index)).thenAnswer(i -> i.getArguments()[0]);
var res = some.some(new byte[]{1, 2}, 4);
Please refer to How do Mockito matchers work?:
Implementation details
Matchers are stored (as Hamcrest-style object matchers) in a stack contained in a class called ArgumentMatcherStorage. MockitoCore and Matchers each own a ThreadSafeMockingProgress instance, which statically contains a ThreadLocal holding MockingProgress instances. It's this MockingProgressImpl that holds a concrete ArgumentMatcherStorageImpl. Consequently, mock and matcher state is static but thread-scoped consistently between the Mockito and Matchers classes.
Most matcher calls only add to this stack, with an exception for matchers like and, or, and not. This perfectly corresponds to (and relies on) the evaluation order of Java, which evaluates arguments left-to-right before invoking a method:
when(foo.quux(anyInt(), and(gt(10), lt(20)))).thenReturn(true);
[6] [5] [1] [4] [2] [3]
This will:
Add anyInt() to the stack.
Add gt(10) to the stack.
Add lt(20) to the stack.
Remove gt(10) and lt(20) and add and(gt(10), lt(20)).
Call foo.quux(0, 0), which (unless otherwise stubbed) returns the default value false. Internally Mockito marks quux(int, int) as the most recent call.
Call when(false), which discards its argument and prepares to stub method quux(int, int) identified in 5. The only two valid states are with stack length 0 (equality) or 2 (matchers), and there are two matchers on the stack (steps 1 and 4), so Mockito stubs the method with an any() matcher for its first argument and and(gt(10), lt(20)) for its second argument and clears the stack.
This demonstrates a few rules:
Mockito can't tell the difference between quux(anyInt(), 0) and quux(0, anyInt()). They both look like a call to quux(0, 0) with one int matcher on the stack. Consequently, if you use one matcher, you have to match all arguments.
Call order isn't just important, it's what makes this all work. Extracting matchers to variables generally doesn't work, because it usually changes the call order. Extracting matchers to methods, however, works great.
int between10And20 = and(gt(10), lt(20));
/* BAD */ when(foo.quux(anyInt(), between10And20)).thenReturn(true);
// Mockito sees the stack as the opposite: and(gt(10), lt(20)), anyInt().
public static int anyIntBetween10And20() { return and(gt(10), lt(20)); }
/* OK */ when(foo.quux(anyInt(), anyIntBetween10And20())).thenReturn(true);
// The helper method calls the matcher methods in the right order.
See this code:
COleDateTime CSpecialEventDlg::GetSpecialEventDate() noexcept
{
COleDateTime datEvent;
if (datEvent.SetDateTime(m_datEvent.GetYear(),
m_datEvent.GetMonth(),
m_datEvent.GetDay(),
0, 0, 0) != 0)
{
// Error
}
return datEvent;
}
My code analysis said I could add noexcept (which I did). But I decided to investigate a little more. I noticed that SetDateTime returns a value:
Zero if the value of this COleDateTime object was set successfully; otherwise, 1. This return value is based on the DateTimeStatus enumerated type. For more information, see the SetStatus member function.
For that latter function (SetStatus) it states:
The status parameter value is defined by the DateTimeStatus enumerated type, which is defined within the COleDateTime class. See COleDateTime::GetStatus for details.
Now, the documentation for GetStatus is documented to have a definition of:
DateTimeStatus GetStatus() const throw();
So, it throws an exception if there is an error. Therefore, I decided to look at the MFC source for SetDateTime:
inline int COleDateTime::SetDateTime(
_In_ int nYear,
_In_ int nMonth,
_In_ int nDay,
_In_ int nHour,
_In_ int nMin,
_In_ int nSec) throw()
{
SYSTEMTIME st;
::ZeroMemory(&st, sizeof(SYSTEMTIME));
st.wYear = WORD(nYear);
st.wMonth = WORD(nMonth);
st.wDay = WORD(nDay);
st.wHour = WORD(nHour);
st.wMinute = WORD(nMin);
st.wSecond = WORD(nSec);
m_status = ConvertSystemTimeToVariantTime(st) ? valid : invalid;
return m_status;
}
It uses ConvertSystemTimeToVariantTime to set the status. That uses:
inline BOOL COleDateTime::ConvertSystemTimeToVariantTime(_In_ const SYSTEMTIME& systimeSrc)
{
return AtlConvertSystemTimeToVariantTime(systimeSrc,&m_dt);
}
And that uses:
inline BOOL AtlConvertSystemTimeToVariantTime(
_In_ const SYSTEMTIME& systimeSrc,
_Out_ double* pVarDtTm)
{
ATLENSURE(pVarDtTm!=NULL);
//Convert using ::SystemTimeToVariantTime and store the result in pVarDtTm then
//convert variant time back to system time and compare to original system time.
BOOL ok = ::SystemTimeToVariantTime(const_cast<SYSTEMTIME*>(&systimeSrc), pVarDtTm);
SYSTEMTIME sysTime;
::ZeroMemory(&sysTime, sizeof(SYSTEMTIME));
ok = ok && ::VariantTimeToSystemTime(*pVarDtTm, &sysTime);
ok = ok && (systimeSrc.wYear == sysTime.wYear &&
systimeSrc.wMonth == sysTime.wMonth &&
systimeSrc.wDay == sysTime.wDay &&
systimeSrc.wHour == sysTime.wHour &&
systimeSrc.wMinute == sysTime.wMinute &&
systimeSrc.wSecond == sysTime.wSecond);
return ok;
}
At this point I have got lost. In short, I am assuming that SetDateTime does not throw an exception.
I understand this much, if I decide to make a call to GetStatus inside the if clause then we do have a potential for exception to be thrown.
All three MFC functions for which you have shown the source code (COleDateTime::SetDateTime, COleDateTime::ConvertSystemTimeToVariantTime and AtlConvertSystemTimeToVariantTime) have – according to their declarations – the potential to throw exceptions (because they have no specification to the contrary, such as noexcept).
However, that doesn't mean that they will (or are even likely to). Digging a bit further into the MFC source code, the only place I can see, in those three functions, where an exception could be thrown is in the ATLENSURE(pVarDtTm!=NULL); line (in the third function).
The ATLENSURE macro is defined as follows:
#define ATLENSURE(expr) ATLENSURE_THROW(expr, E_FAIL)
And ATLENSURE_THROW is, in turn, defined thus:
#define ATLENSURE_THROW(expr, hr) \
do { \
int __atl_condVal=!!(expr); \
ATLASSUME(__atl_condVal); \
if(!(__atl_condVal)) AtlThrow(hr); \
} __pragma(warning(suppress:4127)) while (0)
So, it would seem that, in your code, an exception will be thrown if expr (in the above snippets) is null (the double-bang, !! pseudo-operator makes any non-zero value into 1 and leaves a zero as zero). That expr is the result of the pVarDtTm!=NULL expression, which can only be 0 (false) if the &m_dt argument in the call in your second MFC source excerpt is itself NULL – and, as the address of a member of the class object through which it is being called, that seems improbable (if not impossible).
Another issue you have is that you appear to misunderstand what the throw() specification in the DateTimeStatus GetStatus() const throw(); declaration means. As described here, it is actually (since C++17) an alias for noexcept (or noexcept(true), to be more precise). To specify that a function can throw any type of exception, the throw(...) or noexcept(false) specifications should be used (or use no except/throw specification at all).
So, your last sentence is not really true:
I understand this much, if I decide to make a call to GetStatus inside the if clause then we do have a potential for exception to be
thrown.
Because the GetStatus() function is specified explicitly as non-throwing, and you already have a call to the SetDateTime member function, which (as described above) can throw an exception (but won't, in your code).
I've a function to modify a PictureBox, so I need to use a delegate. My function needs an int in order to do its job, and I've created an enum in order to define the values it can have.
However, when I'm invoking it, there's a problem because it cannot convert from my enum to object, in order to send it the the function.
How can I face it?
My function:
System::Void modifyButtonPicture(int estado)
The enum:
enum BUTTON_STATE : int { PB_STOP = 0, PB_PLAY = 1 };
Delegate:
delegate void SetTextDelegatePlayButton(int estado);
Invoke:
Invoke(gcnew SetTextDelegatePlayButton(this, &Form1:: modifyButtonPicture), PB_PLAY);
The error message (translated):
error C2664: 'System::Object ^System::Windows::Forms::Control::Invoke(System::Delegate ^,...cli::array<Type> ^)' : cannot convert 2nd parameter from 'BUTTON_STATE' to 'System::Object ^'
As documented by MSDN Control::Invoke Method (Delegate, array) the Invoke method accepts these parameters:
method
Type: System::Delegate
A delegate to a method that takes parameters of the same number and type that >are contained in the args parameter.
args
Type: array
An array of objects to pass as arguments to the specified method. This parameter can be nullptr if the method takes no arguments.
And in your call, your passing an int as the second parameter (the PB_PLAY).
So you need to cast your enum to an System::Object array:
int play = (int)PB_PLAY;
array<Object^>^myEnumArray = {play};
Invoke(gcnew SetTextDelegatePlayButton(this, &Form1:: modifyButtonPicture), myEnumArray);
I have a list of KeyValuePairs. I normally would use ToDictionary.
However I just noted that the error message (shown below) has something about explicit cast, which implies I can actually cast the list to Dictionary<...>. How can I do this?
Cannot implicitly convert type 'System.Linq.IOrderedEnumerable<System.Collections.Generic.KeyValuePair<int,string>>' to 'System.Collections.Generic.Dictionary<int, string>'. An explicit conversion exists (are you missing a cast?)
Sample code:
Dictionary<int, string> d = new Dictionary<int, string>() {
{3, "C"},
{2, "B"},
{1, "A"},
};
var s = d.OrderBy(i => i.Value);
d = s;
Implies I can actually cast list to dictionary
Well, it implies that the cast would be valid at compile-time. It doesn't mean it will work at execution time.
It's possible that this code could work:
IOrderedEnumerable<KeyValuePair<string, string>> pairs = GetPairs();
Dictionary<string, string> dictionary = (Dictionary<string, string>) pairs;
... but only if the value returned by GetPairs() were a class derived from Dictionary<,> which also implemented IOrderedEnumerable<KeyValuePair<string, string>>. It's very unlikely that that's actually the case in normal code. The compiler can't stop you from trying, but it won't end well. (In particular, if you do it with the code in your question and with standard LINQ to Objects, it will definitely fail at execution time.)
You should stick with ToDictionary... although you should also be aware that you'll lose the ordering, so there's no point in ordering it to start with.
To show this with the code in your question:
Dictionary<int, string> d = new Dictionary<int, string>() {
{3, "C"},
{2, "B"},
{1, "A"},
};
var s = d.OrderBy(i => i.Value);
d = (Dictionary<int, string>) s;
That compiles, but fails at execution time as predicted:
Unhandled Exception: System.InvalidCastException: Unable to cast object of type 'System.Linq.OrderedEnumerable`2[System.Collections.Generic.KeyValuePair`2[System.Int32,System.String],System.String]' to type 'System.Collections.Generic.Dictionary`2[System.Int32,System.String]'.
at Test.Main()
As a bit of background, you can always cast from any interface type to a non-sealed class ("target"), even if that type doesn't implement the interface, because it's possible for another class derived from "target" to implement the interface.
From section 6.2.4 of the C# 5 specification:
The explicit reference conversions are:
...
From any class-type S to any interface-type T, provided S is not sealed and provided S does not implement T.
...
(The case where S does implement T is covered by implicit reference conversions.)
If you try to implicitly convert a value and there's no implicit conversion available, but there's an explicit conversion available, the compiler will give you the warning in your question. That means you can fix the compiler-error with a cast, but you need to be aware of the possibility of it failing at execution time.
Here's an example:
using System;
class Test
{
static void Main()
{
IFormattable x = GetObject();
}
static object GetObject()
{
return DateTime.Now.Second >= 30 ? new object() : 100;
}
}
Error message:
Test.cs(7,26): error CS0266: Cannot implicitly convert type 'object' to
'System.IFormattable'. An explicit conversion exists (are you missing a cast?)
So we can add a cast:
IFormattable x = (IFormattable) GetObject();
At this point, the code will work about half the time - the other half, it'll throw an exception.
I'm using NUnit v2.5 to compare strings that contain composite Unicode characters.
Although comparison itself works fine, a caret indicating first difference seems to be misplaced.
UPD: I've ended up with overridden EqualConstraint that in turn invokes a custom TextMessageWriter, so I no longer need an answer. See for solution below.
Here's the snippet:
string s1 = "ใช้งานง่าย";
string s2 = "ใช้งานงาย";
Assert.That(s1, Is.EqualTo(s2));
Here's the output:
Expected: "ใช้งานงาย"
But was: "ใช้งานง่าย"
------------------^
The arrow indicating first different character seems to be off 2 positions (as many as there are tone marks above). For longer strings, it becomes a real pain.
I have attempted String.Normalize() but it wouldn't work either.
How can I overcome this problem? Thanks for your help. See my answer below.
When you are comparing Unicode strings, you must always normalize both sides of the comparison, and in the same way. It is not good enough to do binary compare of s1 and s2, because canonically equivalent strings would not test binary equivalent.
Positing the existence of four trivial normalization function, one for each of the four normalization forms, you would want to test NFD(s1) for binary eqality to NFD(s2). It doesn't matter whether you use NFD or NFC there, but you must do the same thing to both strings.
For the k-compat functions, NFKD and NFKD, those are useful when doing string searching, because they improve the recall at the cost of some precision. For example NFKD("™") would be equal to NFKD("TM"). This is what Adobe Reader does, for example, when you run searches on documents: it always runs the search in k-compat mode, so that your searches have a better chance at finding things. However, unlike NFC and NFD, the k-compat functions NFKC and NFKD lose information and are not reversible. With simple NFD and NFC, though, you can always get back to the other one.
You should be able to use the code from this answer to convert each string to an escaped version of the original string. Composite characters will become a single escaped \u codepoint, while combining characters will be a series of such escapes. Then run your Assert on these escaped versions of the string.
I think I cannot find any better answer, so answering my own question.
Cause.
There are many languages using non-spacing modifiers for characters. For European languages, there are substitutions, e.g. "u" (U+0075) + "¨" (U+00A8) = "ü" (U+00FC). In this case, solution by #tchrist is quite sufficient.
However, for complex writing systems, there is no substitution for non-spacing modifiers. Therefore, NUnit's TextMessageWriter.WriteCaretLine(int mismatch) treats mismatch parameter as a byte offset, while screen representation of Thai string may be shorter than the length of caret line ("-----^").
Solution.
Force WriteCaretLine(int mismatch) to respect non-spacing modifiers, reducing mismatch value to the number of non-spacing modifiers occurred before this offset.
Implement all supplementary classes that are actually needed only to make your new code invoked.
Along with Thai, I have tested it with Devanagari and Tibetan. It works as expected.
Yet another pitfall. If you're using NUnit with Visual Studio through ReSharper like I do, you have to configure your Internet Explorer's fonts (it cannot be managed with R#) so that it used proper monospaced fonts for Thai, Devanagari, etc.
Implementation.
Inherit TextMessageWriter and override its DisplayStringDifferences;
Implement your own ClipExpectedAndActual and FindMismatchPosition - here are non-spacing modifiers are respected; Proper clipping is needed since it may also impact calculation of non-spacing elements.
Inherit EqualConstraint and override its WriteMessageTo(MessageWriter writer) so that your MessageWriter was used;
Optionally, create a custom wrapper for simple invocation of custom constraint.
The source code goes below. About 80% of the code doesn't do anything useful, but it's included due to access levels in original code.
// Step 1.
public class ThaiMessageWriter : TextMessageWriter
{
/// <summary>
/// This method is merely a copy of the original method taken from NUnit sources,
/// except that it changes meaning of <paramref name="mismatch"/> before the caret line is displayed.
/// <remarks>
/// Originally passed <paramref name="mismatch"/> contains byte offset, while proper display of caret requires
/// it position to be calculated in character placeholder units. They are different in case of
/// over- or under-string Unicode characters like acute mark or complex script (Thai)
/// </remarks>
/// </summary>
/// <param name="clipping"></param>
public override void DisplayStringDifferences(string expected, string actual, int mismatch, bool ignoreCase, bool clipping)
{
// Maximum string we can display without truncating
int maxDisplayLength = MaxLineLength
- PrefixLength // Allow for prefix
- 2; // 2 quotation marks
int mismatchOffset = mismatch;
if (clipping)
MsgUtils2.ClipExpectedAndActual(ref expected, ref actual, maxDisplayLength, mismatchOffset);
expected = MsgUtils.EscapeControlChars(expected);
actual = MsgUtils.EscapeControlChars(actual);
// The mismatch position may have changed due to clipping or white space conversion
int mismatchInCharPlaceholders = MsgUtils2.FindMismatchPosition(expected, actual, 0, ignoreCase);
Write(Pfx_Expected);
WriteExpectedValue(expected);
if (ignoreCase)
WriteModifier("ignoring case");
WriteLine();
WriteActualLine(actual);
//DisplayDifferences(expected, actual);
if (mismatch >= 0)
WriteCaretLine(mismatchInCharPlaceholders);
}
// Copied due to private
/// <summary>
/// Write the generic 'Actual' line for a constraint
/// </summary>
/// <param name="constraint">The constraint for which the actual value is to be written</param>
private void WriteActualLine(Constraint constraint)
{
Write(Pfx_Actual);
constraint.WriteActualValueTo(this);
WriteLine();
}
// Copied due to private
/// <summary>
/// Write the generic 'Actual' line for a given value
/// </summary>
/// <param name="actual">The actual value causing a failure</param>
private void WriteActualLine(object actual)
{
Write(Pfx_Actual);
WriteActualValue(actual);
WriteLine();
}
// Copied due to private
private void WriteCaretLine(int mismatch)
{
// We subtract 2 for the initial 2 blanks and add back 1 for the initial quote
WriteLine(" {0}^", new string('-', PrefixLength + mismatch - 2 + 1));
}
}
// Step 2.
public static class MsgUtils2
{
private static readonly string ELLIPSIS = "...";
/// <summary>
/// Almost a copy of MsgUtil.ClipExpectedAndActual method
/// </summary>
/// <param name="expected"></param>
/// <param name="actual"></param>
/// <param name="maxDisplayLength"></param>
/// <param name="mismatch"></param>
public static void ClipExpectedAndActual(ref string expected, ref string actual, int maxDisplayLength, int mismatch)
{
// Case 1: Both strings fit on line
int maxStringLength = Math.Max(expected.Length, actual.Length);
if (maxStringLength <= maxDisplayLength)
return;
// Case 2: Assume that the tail of each string fits on line
int clipLength = maxDisplayLength - ELLIPSIS.Length;
int clipStart = maxStringLength - clipLength;
// Case 3: If it doesn't, center the mismatch position
if (clipStart > mismatch)
clipStart = Math.Max(0, mismatch - clipLength / 2);
// shift both clipStart and maxDisplayLength if they split non-placeholding symbol
AdjustForNonPlaceholdingCharacter(expected, ref clipStart);
AdjustForNonPlaceholdingCharacter(expected, ref maxDisplayLength);
expected = MsgUtils.ClipString(expected, maxDisplayLength, clipStart);
actual = MsgUtils.ClipString(actual, maxDisplayLength, clipStart);
}
private static void AdjustForNonPlaceholdingCharacter(string expected, ref int index)
{
while (index > 0 && CharUnicodeInfo.GetUnicodeCategory(expected[index]) == UnicodeCategory.NonSpacingMark)
{
index--;
}
}
static public int FindMismatchPosition(string expected, string actual, int istart, bool ignoreCase)
{
int length = Math.Min(expected.Length, actual.Length);
string s1 = ignoreCase ? expected.ToLower() : expected;
string s2 = ignoreCase ? actual.ToLower() : actual;
int iSpacingCharacters = 0;
for (int i = 0; i < istart; i++)
{
if (CharUnicodeInfo.GetUnicodeCategory(s1[i]) != UnicodeCategory.NonSpacingMark)
iSpacingCharacters++;
}
for (int i = istart; i < length; i++)
{
if (s1[i] != s2[i])
return iSpacingCharacters;
if (CharUnicodeInfo.GetUnicodeCategory(s1[i]) != UnicodeCategory.NonSpacingMark)
iSpacingCharacters++;
}
//
// Strings have same content up to the length of the shorter string.
// Mismatch occurs because string lengths are different, so show
// that they start differing where the shortest string ends
//
if (expected.Length != actual.Length)
return length;
//
// Same strings : We shouldn't get here
//
return -1;
}
}
// Step 3.
public class ThaiEqualConstraint : EqualConstraint
{
private readonly string _expected;
// WTF expected is private?
public ThaiEqualConstraint(string expected) : base(expected)
{
_expected = expected;
}
public override void WriteMessageTo(MessageWriter writer)
{
// redirect output to customized MessageWriter
var myMessageWriter = new ThaiMessageWriter();
base.WriteMessageTo(myMessageWriter);
writer.Write(myMessageWriter);
}
}
// Step 4.
public static class ThaiText
{
public static EqualConstraint IsEqual(string expected)
{
return new ThaiEqualConstraint(expected);
}
}