Zwracana wartość funkcji w PowerShell

188

Opracowałem funkcję PowerShell, która wykonuje szereg działań związanych z udostępnianiem witryn zespołu SharePoint . Ostatecznie chcę, aby funkcja zwróciła adres URL zabezpieczonej witryny jako ciąg znaków, więc na końcu mojej funkcji mam następujący kod:

$rs = $url.ToString();
return $rs;

Kod wywołujący tę funkcję wygląda następująco:

$returnURL = MyFunction -param 1 ...

Spodziewam się String, jednak tak nie jest. Zamiast tego jest to obiekt typu System.Management.Automation.PSMethod. Dlaczego zwraca ten typ zamiast typu String?

ChiliYago
źródło
2
Czy możemy zobaczyć deklarację funkcji?
zdan
1
Nie, nie wywołanie funkcji, pokaż nam, jak / gdzie zadeklarowałeś „MyFunction”. Co się stanie, gdy to zrobisz: Funkcja Get-Item: \ MyFunction
zdan
1
Sugerowany alternatywny sposób rozwiązania tego problemu: stackoverflow.com/a/42842865/992301
Feru
Myślę, że jest to jedno z najważniejszych pytań PowerShell dotyczących przepełnienia stosu związanych z funkcjami / metodami. Jeśli planujesz nauczyć się skryptowania PowerShell, powinieneś przeczytać całą tę stronę i dokładnie ją zrozumieć. Nienawidzisz się później, jeśli tego nie zrobisz.
Birdman

Odpowiedzi:

271

PowerShell ma naprawdę zwariowaną semantykę zwrotów - przynajmniej patrząc z bardziej tradycyjnej perspektywy programowania. Istnieją dwa główne pomysły na obejście głowy:

  • Wszystkie dane wyjściowe są przechwytywane i zwracane
  • Słowo kluczowe return naprawdę wskazuje tylko logiczny punkt wyjścia

Tak więc następujące dwa bloki skryptów wykonają dokładnie to samo:

$a = "Hello, World"
return $a

 

$a = "Hello, World"
$a
return

Zmienna $ a w drugim przykładzie jest pozostawiana jako dane wyjściowe w potoku i, jak wspomniano, zwracane są wszystkie dane wyjściowe. W rzeczywistości w drugim przykładzie można całkowicie pominąć zwrot i uzyskać takie samo zachowanie (zwrot byłby sugerowany, ponieważ funkcja naturalnie kończy się i kończy).

Bez większej ilości definicji funkcji nie mogę powiedzieć, dlaczego otrzymujesz obiekt PSMethod. Domyślam się, że prawdopodobnie masz kilka linii w górę, co nie jest przechwytywane i jest umieszczane w potoku wyjściowym.

Warto również zauważyć, że prawdopodobnie nie potrzebujesz tych średników - chyba że zagnieżdżasz wiele wyrażeń w jednym wierszu.

Możesz przeczytać więcej na temat semantyki zwrotów na stronie about_Return w TechNet lub przez wywołanie help returnpolecenia z samej PowerShell.

Goyuix
źródło
13
więc czy jest jakiś sposób na zwrócenie wartości skalera z funkcji?
ChiliYago
10
Jeśli twoja funkcja zwraca tablicę haszującą, wówczas potraktuje wynik jako taki, ale jeśli „echo” cokolwiek w treści funkcji, w tym wynik innych poleceń, i nagle wynik jest mieszany.
Luke Puplett
5
Jeśli chcesz wypisywać rzeczy na ekran z funkcji bez zwracania tego tekstu jako części wyniku funkcji, użyj polecenia write-debugpo ustawieniu zmiennej $DebugPreference = "Continue"zamiast"SilentlyContinue"
BeowulfNode42
6
To pomogło, ale ten stackoverflow.com/questions/22663848/... pomógł jeszcze bardziej. Potokuj niechciane wyniki poleceń / wywołań funkcji w wywoływanej funkcji przez „... | Out-Null”, a następnie po prostu zwróć żądaną wartość.
Straff,
1
@LukePuplett echobył dla mnie problemem! Ten mały punkt boczny jest bardzo bardzo ważny. Zwracałem tablicę mieszającą, śledząc wszystko inne, ale nadal wartość zwracana nie była tym, czego się spodziewałem. Usunięto echoi działało jak urok.
Ayo I
54

Ta część PowerShell jest prawdopodobnie najgłupszym aspektem. Wszelkie zewnętrzne dane wyjściowe generowane podczas funkcji będą zanieczyszczać wynik. Czasami nie ma żadnych danych wyjściowych, a następnie w pewnych warunkach jest jeszcze kilka nieplanowanych wyników, oprócz planowanej wartości zwrotu.

Tak więc usuwam przypisanie z oryginalnego wywołania funkcji, więc dane wyjściowe kończą się na ekranie, a następnie przechodzę przez kolejne kroki, aż coś, czego nie planowałem, wyskakuje w oknie debugera (przy użyciu programu PowerShell ISE ).

Nawet rzeczy takie jak rezerwowanie zmiennych w zewnętrznych zakresach powodują generowanie danych wyjściowych, jak [boolean]$isEnabledirytujące wyplucie False out, chyba że to zrobisz [boolean]$isEnabled = $false.

Kolejnym dobrym jest to, $someCollection.Add("thing")co wylicza liczbę nowych kolekcji.

Luke Puplett
źródło
2
Dlaczego miałbyś rezerwować nazwę zamiast robić najlepszą praktykę prawidłowego inicjowania zmiennej o wartości wyraźnie zdefiniowanej.
BeowulfNode42
2
Rozumiem, że masz na myśli, że masz blok deklaracji zmiennych na początku każdego zakresu dla każdej zmiennej użytej w tym zakresie. Powinieneś nadal, lub już, inicjalizować wszystkie zmienne przed użyciem ich w dowolnym języku, w tym C #. Co ważne, wykonywanie [boolean]$aw PowerShell nie deklaruje zmiennej $ a. Możesz to potwierdzić, postępując zgodnie z tym wierszem $a.gettype()i porównując dane wyjściowe z zadeklarowanym i zainicjowanym za pomocą łańcucha $a = [boolean]$false.
BeowulfNode42
3
Prawdopodobnie masz rację, wolałbym bardziej jednoznaczny projekt, aby zapobiec przypadkowym zapisom.
Luke Puplett
4
Z perspektywy programisty, chociaż jestem PS-n00b, nie jestem pewien, czy uważam to za najgorszy z wielu irytujących aspektów składni PS. Jak, u licha, ktoś pomyślał, że lepiej napisać „x -gt y” niż „x> y”?!? Z pewnością przynajmniej wbudowane operatory mogły zastosować bardziej przyjazną dla człowieka składnię?
The Dag
5
@TheDag, ponieważ jest to najpierw powłoka, drugi język programowania; >i <są już używane do przekierowywania strumienia we / wy do / z plików. >=zapisuje standardowe wyjście do pliku o nazwie =.
TessellatingHeckler
38

Dzięki PowerShell 5 mamy teraz możliwość tworzenia klas. Zmień swoją funkcję na klasę, a return zwróci tylko obiekt bezpośrednio poprzedzający ją. Oto naprawdę prosty przykład.

class test_class {
    [int]return_what() {
        Write-Output "Hello, World!"
        return 808979
    }
}
$tc = New-Object -TypeName test_class
$tc.return_what()

Gdyby to była funkcja, oczekiwany wynik byłby

Hello World
808979

ale jako klasa zwracana jest tylko liczba całkowita 808979. Klasa jest swego rodzaju gwarancją, że zwróci tylko typ deklarowany lub nieważny.

DaSmokeDog
źródło
6
Uwaga. Obsługuje również staticmetody. Prowadzący do składni[test_class]::return_what()
Sancarn
1
„Gdyby to była funkcja, oczekiwanym wynikiem byłoby„ Hello World \ n808979 ”- Nie sądzę, żeby to było poprawne? Wartość wyjściowa jest zawsze tylko wartością ostatniej instrukcji, w twoim przypadku return 808979, funkcji lub nie.
am
1
@amn Dziękujemy! Masz rację, przykład zwróci to tylko, jeśli utworzy dane wyjściowe w ramach funkcji. Irytuje mnie to. Czasami twoja funkcja może generować dane wyjściowe i zwracane są dane wyjściowe plus dowolna wartość dodana w instrukcji return. Klasy, jak wiesz, zwracają tylko typ zadeklarowany lub nieważny. Jeśli wolisz używać funkcji, jedną z łatwych metod jest przypisanie funkcji do zmiennej, a jeśli zwracana jest więcej niż zwracana wartość, var jest tablicą, a zwracana wartość będzie ostatnim elementem w tablicy.
DaSmokeDog
1
Czego oczekujesz od polecenia o nazwie Write-Output? Nie chodzi tu o wyjście „konsoli”, lecz o wyjście funkcji / cmdlet. Jeśli chcesz zrzucić rzeczy na konsoli istnieją Write-Verbose, Write-Hosta Write-Errorfunkcje, między innymi. Zasadniczo Write-Outputjest bliższy returnsemantyce niż procedurze wyjściowej konsoli. Nie nadużywaj tego, używaj Write-Verbosegadatliwości, po to jest.
am
1
@amn Jestem bardzo świadomy, że to nie jest konsola. Był to mizerny, szybki sposób pokazania, w jaki sposób zwrot radzi sobie z nieoczekiwanymi danymi wyjściowymi wewnątrz funkcji w porównaniu z klasą. w funkcji wszystkie dane wyjściowe + zwracana wartość są zwracane. JEDNAK tylko wartość określona w zwrocie klasy jest przekazywana paczka. Spróbuj uruchomić je dla siebie.
DaSmokeDog
31

Jako obejście zwracałem ostatni obiekt w tablicy, który otrzymałeś z funkcji ... To nie jest świetne rozwiązanie, ale jest lepsze niż nic:

someFunction {
   $a = "hello"
   "Function is running"
   return $a
}

$b = someFunction
$b = $b[($b.count - 1)]  # Or
$b = $b[-1]              # Simpler

Podsumowując, bardziej jednoznaczny sposób pisania tego samego może być:

$b = (someFunction $someParameter $andAnotherOne)[-1]
Jonesy
źródło
7
$b = $b[-1]byłby prostszym sposobem na uzyskanie ostatniego elementu lub tablicy, ale jeszcze prostszym byłoby po prostu nie generowanie wartości, których nie chcesz.
Duncan
@ Duncan A co, jeśli chcę wyprowadzić dane w funkcji, a jednocześnie chcę również wartość zwracaną?
Utkan Gezer
@ThoAppelsin, jeśli masz na myśli, że chcesz napisać coś do terminala, użyj go, write-hostaby wydrukować go bezpośrednio.
Duncan
7

Mijam prosty obiekt Hashtable z jednym elementem wynikowym, aby uniknąć szaleństwa zwrotnego, ponieważ chcę również wyświetlać dane na konsoli. Działa poprzez przekazanie przez odniesienie.

function sample-loop($returnObj) {
  for($i = 0; $i -lt 10; $i++) {
    Write-Host "loop counter: $i"
    $returnObj.result++
  }
}

function main-sample() {
  $countObj = @{ result = 0 }
  sample-loop -returnObj $countObj
  Write-Host "_____________"
  Write-Host "Total = " ($countObj.result)
}

main-sample

Prawdziwe przykładowe użycie można zobaczyć w moim projekcie GitHub unpackTunes .

Co by było fajne
źródło
4

Trudno powiedzieć bez patrzenia na kod. Upewnij się, że twoja funkcja nie zwraca więcej niż jednego obiektu i że przechwytujesz wyniki uzyskane z innych wywołań. Za co dostajesz:

@($returnURL).count

W każdym razie dwie sugestie:

Rzuć obiekt na ciąg:

...
return [string]$rs

Lub po prostu umieść go w podwójnych cudzysłowach, takich samych jak powyżej, ale krótszych do wpisania:

...
return "$rs"
Shay Levy
źródło
1
Lub nawet po prostu "$rs"- returnjest wymagany tylko przy wcześniejszym powrocie z funkcji. Pominięcie zwrotu jest lepszym idiomem PowerShell.
David Clarke
4

Wydaje się, że opis funkcji przez Luke'a w tych scenariuszach jest słuszny. Chcę tylko zrozumieć podstawową przyczynę, a zespół produktu PowerShell zrobiłby coś z tym zachowaniem. Wydaje się być dość powszechny i ​​kosztował mnie zbyt wiele czasu na debugowanie.

Aby obejść ten problem, używałem zmiennych globalnych zamiast zwracania i używania wartości z wywołania funkcji.

Oto kolejne pytanie dotyczące stosowania zmiennych globalnych: Ustawianie globalnej zmiennej PowerShell z funkcji, w której nazwa zmiennej globalnej jest zmienną przekazywaną do funkcji

Ted B.
źródło
3

Poniższe zwraca po prostu 4 jako odpowiedź. Po zastąpieniu dodawanych wyrażeń dla ciągów zwraca pierwszy ciąg.

Function StartingMain {
  $a = 1 + 3
  $b = 2 + 5
  $c = 3 + 7
  Return $a
}

Function StartingEnd($b) {
  Write-Host $b
}

StartingEnd(StartingMain)

Można to również zrobić dla tablicy. Poniższy przykład zwróci „Tekst 2”

Function StartingMain {
  $a = ,@("Text 1","Text 2","Text 3")
  Return $a
}

Function StartingEnd($b) {
  Write-Host $b[1]
}

StartingEnd(StartingMain)

Zauważ, że musisz wywołać funkcję poniżej samej funkcji. W przeciwnym razie przy pierwszym uruchomieniu zwróci błąd, że nie wie, co to jest „StartingMain”.

Edwin
źródło
2

Istniejące odpowiedzi są poprawne, ale czasami nie zwraca się czegoś jawnie za pomocą a Write-Outputlub a return, ale w wynikach funkcji jest pewna tajemnicza wartość. Może to być wynik działania wbudowanej funkcjiNew-Item

PS C:\temp> function ContrivedFolderMakerFunction {
>>    $folderName = [DateTime]::Now.ToFileTime()
>>    $folderPath = Join-Path -Path . -ChildPath $folderName
>>    New-Item -Path $folderPath -ItemType Directory
>>    return $true
>> }
PS C:\temp> $result = ContrivedFolderMakerFunction
PS C:\temp> $result


    Directory: C:\temp


Mode                LastWriteTime         Length Name
----                -------------         ------ ----
d-----         2/9/2020   4:32 PM                132257575335253136
True

Cały ten dodatkowy szum związany z tworzeniem katalogu jest gromadzony i emitowany na wyjściu. Najłatwiejszym sposobem na złagodzenie tego jest dodanie | Out-Nullna końcu New-Iteminstrukcji lub można przypisać wynik do zmiennej i po prostu nie używać tej zmiennej. Wyglądałoby to tak ...

PS C:\temp> function ContrivedFolderMakerFunction {
>>    $folderName = [DateTime]::Now.ToFileTime()
>>    $folderPath = Join-Path -Path . -ChildPath $folderName
>>    New-Item -Path $folderPath -ItemType Directory | Out-Null
>>    # -or-
>>    $throwaway = New-Item -Path $folderPath -ItemType Directory 
>>    return $true
>> }
PS C:\temp> $result = ContrivedFolderMakerFunction
PS C:\temp> $result
True

New-Itemjest prawdopodobnie bardziej znany z nich, ale inne obejmują wszystkie StringBuilder.Append*()metody, a także SqlDataAdapter.Fill()metodę.

StingyJack
źródło