
从基础到中级:数组(二)
概述
在上一篇文章从基础到中级:数组(一)中,我们开始讨论编程中最复杂和最具挑战性的话题之一。我知道很多人可能会说,这实际上很简单,我把它描述为复杂是没有意义的。然而,随着我们的进展,你会明白为什么我断言这个话题确实很复杂,很难掌握。毕竟,它是其他一切的基础。
一旦我解释并演示了如何真正应用这个概念,亲爱的读者,如果你能完全理解将要展示的内容,你无疑会明白为什么编程语言中存在其他功能。因为,一旦你理解了这个基础,其他一切都会变得更容易掌握和理解。
事实上,这里最大的挑战是以一种不深入探讨我们尚未涵盖的其他主题的方式呈现事物。我试图说明为什么创建了某些元素,但还没有真正揭示它们。这是因为理解这些工具背后的基本概念比理解工具本身重要得多。由于许多程序员经常忽视这个概念,过于关注工具,他们有时会陷入僵局。这是因为解决问题的不是工具,而是概念。它就像一把锤子 —— 可以用来钉钉子。但它也可以用于其他目的 —— 例如,拆除东西。尽管可能有更好的拆除工具,比如大锤。
在我们开始之前,有一个必要的先决条件来充分理解这篇文章:了解和理解变量是什么以及常数是什么是至关重要的。
ROM 类型的数组
声明数组基本上有两种方法。一种是声明静态数组,另一种是将其声明为动态数组。虽然在实践中,理解每种类型相对简单,但有一些微妙的细微差别可能会使动态数组和静态数组的真正含义变得复杂,甚至阻碍对它们的清晰理解。尤其是在考虑其他编程语言,如 C 和 C++ 时。然而,即使在 MQL5 中,也可能会有一些时候你发现自己有些不确定。这是因为静态数组和动态数组之间的根本区别在于后者在代码执行过程中改变大小的能力。
这样想,将数组分类为动态或静态似乎很简单。但是,重要的是要记住字符串也是数组。尽管它是一种特殊的数组。这使得将其严格分类为静态或动态变得复杂。尽管如此,让我们把这个事实放在一边。我们不会直接深入研究字符串类型。因此,我们将避免混淆,并确保对这一主题有清晰的理解。
从根本上讲,常数数组始终是静态数组,这在您将来可能学习的任何编程语言中都是无可争议的。无论您使用什么编程语言,它始终是静态的。
但是,为什么我们可以自信地断言常量数组总是静态的呢?原因是常量数组,你需要这样想,本质上就是 ROM 存储器。在这一点上,这可能没有多大意义。毕竟,我们的应用程序总是在 RAM 的一个区域中加载和执行。那么,我们如何想象 ROM 存储器,它只允许我们读取数据,在 RAM 环境中,我们可以在任何时候读取和写入数据?这显然是不合逻辑的。
正因为如此,亲爱的读者,你必须了解常量和变量真正代表什么。从我们的应用程序的角度来看,确实可以在 RAM 中使用类似 ROM 的行为,而不会损害应用程序的完整性或可用性。事实上,在应用程序中使用常量数组创建小型 ROM 相对常见。特别是在更复杂的代码库或需要高度特定值的应用程序中,这些值永远不能改变。
想想用于报告事件或信息的消息。您可以整合这些消息,将其翻译成不同的语言。当加载应用程序时,它可以确定使用哪种语言并相应地构建 ROM 存储器。这将允许不同的用户使用该应用程序,每个用户都有不同的语言偏好。这是一个经典的例子,说明数组在 ROM 中的行为方式。但是,如果您改为使用文件动态加载翻译,则数组将不再纯粹是 ROM,或者纯静态的。虽然它仍然可以这样分类。
为了使这个概念更容易理解,因为理解它对于知道如何以及何时使用标准库调用来管理数组至关重要,让我们来看一个简单的例子。它在实践中展示了这一概念。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. const char Rom_01[] = {72, 101, 108, 111, 33}; 07. const char Rom_02[8] = {'H', 'e', 'l', 'o', '!'}; 08. 09. PrintFormat("%c%c%c%c%c%c", Rom_01[0], Rom_01[1], Rom_02[2], Rom_01[2], Rom_02[3], Rom_01[4]); 10. } 11. //+------------------------------------------------------------------+
代码 01
当执行代码 01 时,结果如下所示。
图 01
这很容易理解。然而,我们感兴趣的是第六行和第七行。在这两行中,我们都创建了两个 ROM,每个 ROM 都具有相同的明显内容。但它们的大小却完全不同。但请等一等。怎么会这样?我不明白。查看代码,我可以看到这两个数组都有五个元素。并且那里显示的所有元素都是相同的。只是它们是以不同的方式声明的。但为什么这两个 ROM 不同呢?这完全不合理。
亲爱的读者,你说得对,两个数组都声明了相同的元素,尽管方式不同。但它们的不同之处恰恰在于它们的声明方式。现在,请密切关注这里,了解一个重要的问题。
ROM_01 在第六行声明,包含五个元素。通常,在创建静态数组的代码中,我们会看到这种声明。请注意,括号内没有指定值。但是(这就是许多人感到困惑的地方)不在常量数组的括号内声明值是完全正常的,甚至相当常见。这是因为它允许您在实现过程中向数组添加更多或更少的值。一旦设置了这些值,它们就会完全锁定,无法修改。因此,数组的大小固定在存在的元素数量上。
现在,有了 ROM_02,情况就有些不同了。在这种情况下,明确声明了五个元素。然而,由于我们指定数组有八个元素,其中三个元素仍然未定义。使用这些未定义的元素时需要小心,这是因为它们可能包含有效数据,也可能包含完全随机的值。这将取决于编译器如何初始化数组。请记住,我们在这里处理的是常量数组。
无论如何,在这两种情况下,我们都在使用静态数组。也就是说,在数组的整个生命周期内,元素的数量不会改变。在一种情况下,我们总是有五个元素,在另一种情况中,总是有八个元素。请记住第一个元素的索引为 0。因此计数总是从 0 开始。
好的,我们知道每个数组中有多少个元素。但是如果我们尝试访问索引 5 处的元素会发生什么?记住,计数从 0 开始。好吧,在这种情况下,我们可以运行一个小测试,这样你就可以观察到发生了什么。为此,让我们更改代码中的某些内容,使其看起来如下图所示。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. const char Rom_01[] = {72, 101, 108, 111, 33}; 07. const char Rom_02[8] = {'H', 'e', 'l', 'o', '!'}; 08. 09. const uchar pos = 6; 10. 11. PrintFormat("%c%c%c%c%c%c", Rom_01[0], Rom_01[1], Rom_02[2], Rom_01[2], Rom_02[3], Rom_01[4]); 12. 13. PrintFormat("Contents of position %d of the ROM_02 array: %d", pos, Rom_02[pos - 1]); 14. PrintFormat("Contents of position %d of the ROM_01 array: %d", pos, Rom_01[pos - 1]); 15. } 16. //+------------------------------------------------------------------+
代码 02
请注意,现在在代码 02 的第九行,我们有一个常数,指示我们试图访问哪个元素。在这种情况下,我们想要第 6 个元素。它位于索引为 5 的位置。您可能会想,“好吧,因为我们正在寻找位置 5 的元素,并且我们已经声明了 5 个元素,并且我们想要打印该元素的十进制值,所以第 13 行和第 14 行都会打印相同的值 - 在本例中为 33。”事实上,这是最自然的思考方式。但这是错误的。这是因为计数从 0 开始。因此,第五个声明的元素实际上是索引为 4 的元素。因此,当我们尝试访问索引 5 时,代码中就会发生一些事情。结果如下所示。
图 02
请注意,这里发生了两件奇怪的事情。首先,第 13 行被执行,并且显示的结果为 0。这意味着第 7 行声明的数组包含隐藏值。但关键就在于这张图 02 中所看到的错误信息。它告诉我们,在代码 02 的第 14 行,尝试访问数组外部的内容。
由于第 14 行使用的数组是第 6 行声明的数组,您可能会对错误发生的原因感到困惑。发生这种情况正是因为您试图访问第 5 个位置的元素。由于数组有 5 个元素,所以这似乎是可能的。但再次强调,计数从 0 开始。
为了确认这一点,让我们做出修改。此修改位于代码 02 的第 9 行。通过将那里看到的值 6 更改为值 5,如下面的代码行所示,一切都会改变。
const uchar pos = 5;
当我们再次编译并运行代码 02 时,使用上面显示的更新代码行,终端中的结果如下所示。
图 03
您是否注意到代码现在能够正确显示内容?有了这个,我相信您将能够理解我们应该如何访问数组。数组声明中的微小差异如何导致我们得到完全不同的结果。
由于 ROM 类型数组始终是静态的,即使以可能看起来是动态的方式声明,这里也没有太多要说的。这是因为这种类型的数组,任何写入它的尝试都会导致编译错误,这让我们几乎没有什么可讨论的了。所以,让我们继续讨论一个新主题,我们将介绍另一种类型的数组 —— 一种稍微复杂一点的数组。这是因为我们可以向其写入信息。
RAM 类型数组
理解这种数组的前提其实就是理解 ROM 类型的数组。声明可以是静态的,也可以是动态的。然而,与 ROM 类型数组不同,其内容是恒定的,其元素数量也是固定的,而 RAM 类型数组的情况则变得有点复杂。这是因为元素的数量可能是恒定的,也可能不是恒定的。此外,每个元素的内容可以随着代码的执行而变化。
现在,有了这些介绍性的词语,你可能会认为这种类型的数组是一场活生生的噩梦。然而,它只是需要更多的注意才能正确使用。这就是每一个细节都有区别的地方。但是,亲爱的读者,我想让你暂时忽略 MQL5 中数组的某些细节。这是因为有一种特殊类型的数组可以用作缓冲区。这是 MQL5 所特有的。但是,当我们深入研究专为 MetaTrader 5 设计的计算和编程时,这种类型将在另一个时间得到更好的解释。所以现在,我们在这里解释的内容不适用于这些缓冲区类型的数组。此时,这里的目的是提供编程中数组的一般解释。不仅在 MQL5 中,而且在所有编程语言中。
好的,让我们执行以下操作:根据上一主题中看到的最后一段代码,让我们从两个声明中删除保留字 “const”。通过这样做,我们消除了对数组施加的限制。然而,当我们进行这一修改时,有一个非常重要的细节需要解释,这将在接下来显示的代码中涉及。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. char Ram_01[] = {72, 101, 108, 111, 33}; 07. char Ram_02[8] = {'H', 'e', 'l', 'o', '!'}; 08. 09. const uchar pos = 5; 10. 11. PrintFormat("%c%c%c%c%c%c", Ram_01[0], Ram_01[1], Ram_02[2], Ram_01[2], Ram_02[3], Ram_01[4]); 12. 13. PrintFormat("Contents of position %d of the RAM_02 array: %d", pos, Ram_02[pos - 1]); 14. PrintFormat("Contents of position %d of the RAM_01 array: %d", pos, Ram_01[pos - 1]); 15. } 16. //+------------------------------------------------------------------+
代码 03
当你运行这个代码 03 的时候,你会得到和图 03 一样的结果。但有一个根本的区别,就是在于我们不使用 ROM。相反,我们使用的是 RAM。这使我们能够更改代码 03 来创建如下所示的内容。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. char Ram_01[] = {72, 101, 108, 111, 33}; 07. char Ram_02[8] = {'H', 'e', 'l', 'o', '!'}; 08. 09. uchar pos = 5; 10. 11. PrintFormat("%c%c%c%c%c%c", Ram_01[0], Ram_01[1], Ram_02[2], Ram_01[2], Ram_02[3], Ram_01[4]); 12. 13. PrintFormat("Contents of position %d of the Ram_02 array: %d", pos, Ram_02[pos - 1]); 14. PrintFormat("Contents of position %d of the Ram_01 array: %d", pos, Ram_01[pos - 1]); 15. 16. Ram_02[pos - 1] = '$'; 17. PrintFormat("Contents of position %d of the Ram_02 array: %d", pos, Ram_02[pos - 1]); 18. 19. Ram_01[pos - 1] = '$'; 20. PrintFormat("Contents of position %d of the Ram_01 array: %d", pos, Ram_01[pos - 1]); 21. } 22. //+------------------------------------------------------------------+
代码 04
这个代码 04 相当奇怪,而且在某种程度上甚至有点耐人寻味。这仅仅是因为在第 16 行和第 19 行,我们试图修改数组中给定位置的值。这是一个细节,如果理解得当,确实可以激发我们中更大胆的人的好奇心。因此,当我们运行代码 04 时,结果如下所示。
图 04
请注意,确实可以修改这些值。如果我们使用 ROM 类型的数组,这是不允许的。此时,您可能会想:我们可以更改数组中的任何信息吗?那么,这个问题的答案是:这视情况而定。
亲爱的读者,请注意以下内容:第 6 行的数组虽然是动态的,但由于我们没有指定明确的元素计数,因此保持静态。这是因为我们在声明时对其进行了初始化。第 7 行的数组完全是静态的,但其中可能元素的数量大于我们在数组初始化期间实际声明的元素数量。
理解了这一点,你可能会说:因此,如果我们使用一个指向元素的索引,我们可以更改它的值。是这样吗?正是这样。事实上,您可以使用类似于下面所示的代码。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. char r[10] = {72, 101, 108, 108, 111, 33}; 07. 08. string sz0; 09. 10. sz0 = ""; 11. for (uchar c = 0; c < ArraySize(r); c++) 12. sz0 = StringFormat("%s%03d, ", sz0, r[c]); 13. 14. Print(sz0); 15. 16. Print("Modifying a value..."); 17. 18. r[7] = 36; 19. 20. sz0 = ""; 21. for (uchar c = 0; c < ArraySize(r); c++) 22. sz0 = StringFormat("%s%03d, ", sz0, r[c]); 23. 24. Print(sz0); 25. } 26. //+------------------------------------------------------------------+
代码 05
当我们运行代码 05 时,结果将如下图所示。
图 05
换句话说,它确实有效。我们可以更改任何值,只要我们访问的元素存在于数组中。但是,第 18 行之所以有效,是因为在第 6 行中,我们指定该数组将有 10 个元素。
好的,我相信第一部分已经很清楚了。但我们总是需要这样工作吗?也就是说,在声明数组时,我们是否总是需要指定元素的数量或元素本身?事实上,这是一个在初学者中引起很多怀疑的问题,答案是:这视情况而定。
但是,亲爱的读者,请务必记住这一点:常量数组(即 ROM 类型数组)在创建时必须始终声明其元素或元素数量。一个细节是:在这种情况下,声明元素本身是强制性的,但声明元素的数量是可选的。
此规则严格,不允许更改或解释。然而,对于 RAM 类型的数组,没有严格的规则可遵循。一切都取决于要实现的目的或目标。但是,在动态数组(非 ROM 类型或常量的数组)中声明元素并不常见。正如我们在代码 04 第 6 行看到的那样。这是因为通过这样做,我们基本上将动态数组变成了静态数组。这与同一代码 04 第 7 行的数组发生的情况有些类似。
但是,就代码解释而言,代码 04 第 6 行声明的数组可以用如下所示的内容替换。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. char Ram_01[5], 07. pos = 5; 08. 09. Ram_01[0] = 72; 10. Ram_01[1] = 101; 11. Ram_01[2] = 108; 12. Ram_01[3] = 111; 13. Ram_01[4] = 33; 14. 15. PrintFormat("%c%c%c%c%c%c", Ram_01[0], Ram_01[1], Ram_01[2], Ram_01[2], Ram_01[3], Ram_01[4]); 16. 17. PrintFormat("Contents of position %d of the Ram_01 array: %d", pos, Ram_01[pos - 1]); 18. Ram_01[pos - 1] = '$'; 19. PrintFormat("Contents of position %d of the Ram_01 array: %d", pos, Ram_01[pos - 1]); 20. } 21. //+------------------------------------------------------------------+
代码 06
请注意,在代码 06 中,我们现在在第 6 行有一个静态数组声明。通过这样做,我们指示编译器为我们分配一个内存块。此分配在编译期间自动发生。因此,编译器将在内存中保留一个足够大的空间来包含指定数量的元素。在这种情况下,有 5 个元素。由于类型是 “char”,我们将分配 5 个字节并可立即使用。
现在将这个分配的空间视为一个 5 字节变量。您可以根据需要随意使用它。我们也可以分配任意数字。例如,如果我们使用的不是 5(如第 6 行所示),而是 100,那么我们将创建一个可以被视为 100 字节的变量。请记住,当前最大宽度是 8 字节,或 64 位。换句话说,我们可以将 12 个以上的 8 字节变量放入这个 100 字节的数组中。
无论如何,当您运行代码 06 时,您将看到下面显示的结果。
图 06
现在请注意,初始化不是在数组声明期间完成的,而是在第 9 行和第 13 行之间完成的。其工作方式与代码 06 中的第 18 行相同,并且与前面的示例中所做的一样。
不过,这里有一个小细节。由于数组是以静态方式创建的,如果出于任何原因需要更多空间,您将无法分配额外的内存。此限制在运行时出现。因此,静态数组用于非常特殊的情况。这同样适用于动态数组。但是,如果我们不定义一个值来指示数组中元素的数量(如代码 06 第六行所示),而是使用下面显示的代码行,会怎么样?
char Ram_01[]
这将导致声明一个完全动态的数组。现在,亲爱的读者,请密切注意。通过这样做,我们告诉编译器,我们程序员将负责管理数组。即根据需要分配和释放内存。然而,一执行第 9 行,就会出现错误。这是因为您将尝试访问尚未分配的内存。该错误如下所示。
图 07
遇到这种类型的错误并不罕见,特别是在具有许多变量的程序中。不过,修复它很简单。我们需要做的就是在使用数组之前分配内存。然后,一旦我们用完了数组,我们就应该明确释放内存。这种显式的释放被认为是一种良好的编程实践。
在更简单的程序中,或者在不太专业的代码中,经常会看到跳过此释放步骤。但是,不建议这样做,因为分配的内存不会仅仅因为您停止使用该内存区域而自动释放。因此,最好从一开始就养成良好的习惯。在需要时分配内存,在不再需要时释放内存,以避免代码执行过程中出现奇怪的错误。
最后,让我们看看如何修复代码以正确使用动态数组。我们将代码 06 替换为代码 07。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. char r[]; 07. string sz0; 08. 09. PrintFormat("Allocated enough position for %d elements", ArrayResize(r, 10)); 10. 11. r[0] = 72; 12. r[1] = 101; 13. r[2] = 108; 14. r[3] = 111; 15. r[4] = 33; 16. 17. sz0 = ""; 18. for (uchar c = 0; c < ArraySize(r); c++) 19. sz0 = StringFormat("%s%03d, ", sz0, r[c]); 20. Print(sz0); 21. 22. Print("Modifying a value..."); 23. 24. r[7] = 36; 25. 26. sz0 = ""; 27. for (uchar c = 0; c < ArraySize(r); c++) 28. sz0 = StringFormat("%s%03d, ", sz0, r[c]); 29. Print(sz0); 30. 31. ArrayFree(r); 32. } 33. //+------------------------------------------------------------------+
代码 07
亲爱的读者,这段代码片段确实利用了纯动态数组。请注意它与代码 05 非常相似,这是故意的。这种相似性是有意的,旨在证明我们可以通过不同的方式实现相同的结果。执行代码 07 的结果如下所示。
图 08
但请密切注意图 08 中突出显示的信息。这部分极其重要。通常,不太谨慎的程序员,或者那些编写不用于关键目的的代码的程序员,会在没有正确初始化内存的情况下使用内存。即使对于经验丰富的开发人员来说,这种疏忽也会导致大量难以检测的错误。在这里,我正在演示一个这样的问题。
请注意,内存是在代码 07 的第 9 行分配的。在第 11 行和第 15 行之间,我们为分配的内存中的某些位置分配值。然而,当我们从内存中读取并试图检索其内容时,我们看到了一些奇怪的东西。这些信息被称为垃圾,因为它存在于内存中,但不应该存在。
这里的关键点是第 22 行。它告诉我们,那一刻,我们将修改内存。这发生在第 24 行。然而,再次读取内存时,就好像程序可以预测未来一样。就好像在我们将该值应用于该内存位置之前,该值突然出现。就像魔法一样。
那么,为什么会发生这种情况呢?这不是魔术,也不是时间旅行。原因在于,分配的内存并不真正在您的控制之下。操作系统处理内存分配,内存的内容可以是任何东西。这可能是其他程序剩下的数据(称为垃圾)甚至是一堆 0。内容总是随机的。此外,如果操作系统为连续两次执行分配相同的内存位置,其中内存被分配、释放,然后再次分配,无论是由相同的代码还是完全不同的代码,产生垃圾的可能性都很高。
如果你假设内存的内容是某种方式的,你可能会遇到一个很难解决的问题。因此,永远不要假设内存包含特定的值。绝对不要。分配内存时,务必清理该区域或至少将其完全初始化。在 MQL5 中有很多方法可以做到这一点。就我个人而言,我更喜欢从标准库进行简单高效的调用来处理这个问题。因此,代码 07 的更正版本如下所示。
01. //+------------------------------------------------------------------+ 02. #property copyright "Daniel Jose" 03. //+------------------------------------------------------------------+ 04. void OnStart(void) 05. { 06. char r[]; 07. string sz0; 08. 09. PrintFormat("Allocated enough position for %d elements", ArrayResize(r, 10)); 10. 11. ZeroMemory(r); 12. 13. r[0] = 72; 14. r[1] = 101; 15. r[2] = 108; 16. r[3] = 111; 17. r[4] = 33; 18. 19. sz0 = ""; 20. for (uchar c = 0; c < ArraySize(r); c++) 21. sz0 = StringFormat("%s%03d, ", sz0, r[c]); 22. Print(sz0); 23. 24. Print("Modifying a value..."); 25. 26. r[7] = 36; 27. 28. sz0 = ""; 29. for (uchar c = 0; c < ArraySize(r); c++) 30. sz0 = StringFormat("%s%03d, ", sz0, r[c]); 31. Print(sz0); 32. 33. ArrayFree(r); 34. } 35. //+------------------------------------------------------------------+
代码 08
当您运行代码 08 时,您将获得如下所示的内容。
图片 09
请注意,图 08 和图 09 之间的唯一区别是具体的内存位置。在第一次运行中,我们在该位置遇到了垃圾数据。但在代码 08 中添加第 11 行之后,这种可以使用垃圾的错误不再发生。就这么简单。也就是说,MQL5 中还有其他函数可以实现相同的目的。这一切都取决于你的选择和你想要实现的目标。
最后的探讨
在这篇文章中,我展示了如何从更专业的角度处理数组,这篇文章确实触及了比这里展示的更复杂的基础。虽然你可能没有完全理解我想展示的一切,但这种主题的解释比许多其他编程概念更具挑战性。不过,我相信我已经实现了部分主要目标。我解释了如何在 RAM 中创建类似 ROM 的行为。我们还讨论了在动态和静态数组之间进行选择的决策过程。
话虽如此,强调一个关键点很重要:处理数组或内存访问时永远不要假设任何事情。未初始化的元素可能包含垃圾。在代码中使用这种垃圾会严重影响您的预期结果,或者使解决不同程序之间的交互变得更加困难。
无论如何,所附材料包括我们在这里审查的代码。这将使您能够学习和练习。这也将帮助您学习如何处理故障,并在更实际的任务中探索与数组使用相关的概念。
本文由MetaQuotes Ltd译自葡萄牙语
原文地址: https://www.mql5.com/pt/articles/15472
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.

